--- name: to-triggers description: Turn a plain-language rule into a well-formed vault trigger entry. Interviews intent (remind vs warn vs block), then authors the matching mechanics itself — stemmed keyword choice, anchored regexes, glob semantics, predicate selection — proves fire and near-miss behavior with a dry run, and writes vault.yaml only on explicit approval with kmd validate green after. The human owns the intent; the skill owns the regex. Use when the user says "add a hook", "add a trigger", "add a rule for", "create a gate", "block X until Y", "remind me about X when", "never let X happen before Y", "/to-triggers", or right after a protocol rule failed to fire — the moment a prose rule proves it needs to become a gate. --- # To-Triggers — Author Vault Gate Triggers from Intent Convert a stated rule into a trigger entry in `vault.yaml`. The division of labor is fixed: the user owns the intent ("never let a tag happen before the retro"); the agent owns the matching sophistication. A mis-authored trigger either never fires (silent protocol decay) or fails the `vault.yaml` load (fail-loud: one invalid trigger takes down every tool). Never hand the user a regex question they didn't ask for. ## Source of truth The trigger schema is read at use time, never from memory: - The `wiki` skill's `references/vault-yaml.md` § Harness gate triggers (ships alongside these skills) — field vocabulary and examples. - The vault's own `vault.schema.json` (vault root) and `kmd validate` — the runtime authority. When the reference file is unavailable, these suffice. ## Interview flow — recommend first Draft a complete trigger from the user's sentence, present it, and ask only what could not be inferred. Never open the interview with questions the intent already answers. 1. **Intent class.** Map the sentence: | The user wants | Class | Event | |---|---|---| | a reminder when a topic comes up | `inject` | `prompt` | | a nudge alongside a risky tool call | `inject` or `warn` | `pretool` | | an action denied until a precondition holds | `block` | `pretool` | Only `pretool` can block. A prompt-time rule that sounds like a gate ("never tag before the retro") is really a pretool block on the tagging command plus, optionally, a prompt-time reminder — propose both, let the user drop one. 2. **Matching draft.** Author the mechanics (rules below) with the trigger `id` (descriptive kebab-case; duplicate ids keep the first occurrence). 3. **Payload register.** `text` (inject) and `reason` (block) are read by an agent mid-session: one ASCII line naming the protocol pointer, no banners. A block `reason` tells the denied agent what to do instead, not just why. 4. **Scope.** Propose where it lands: the active scope's `triggers_extra` (additive), or `triggers_extra._all` for vault-wide. A full-replace `triggers` section only on explicit request — it drops the engine defaults and the compiled file source too. 5. **Precondition.** When the rule is state-dependent ("until the retro is newer than the release note"), attach `when: newer-than {fresh, than}` — the trigger fires when the predicate is FALSE, is suppressed when true, and is skipped loudly when unevaluable. The clock is frontmatter `updated`; an empty `than` set passes vacuously. Globs select the two page sets; each scope names its own retro/release conventions. ## Matching mechanics — the part the user never writes **Keywords (prompt inject).** The engine matches through a stemmed, word-boundary FTS5 porter table. Consequences: - Two or three distinctive keywords beat a long list. Never enumerate word forms — `release` already matches "releasing", "released". - Word-boundary matching kills substring false positives ("prerelease" does not match `release`); don't defend against them with regex. - `intent` regexes are the escape hatch for phrasings stemming cannot reach, not the default. **Pretool matchers.** AND-composed, deterministic: - `tool` equals the event's tool name. - `args_match` is an authored, anchored regex over the serialized tool input (compound commands included). Draft it against real command shapes — for "block force pushes": matches `git push --force origin main` and `git push -f`, does not match `echo "force push"`. Write the near-miss counterexamples down; they become the dry-run cases. - `files` globs are cwd-relativized: `**` crosses directories (`**/` may match empty), `*` stays within a segment, `?` is one character. **Noise budget.** Name the cost of a broad draft before accepting it: inject-class dedup defaults to once per session per trigger, so a broad keyword still fires in every session; block-class is exempt from dedup and fires on every matching event. Propose the narrower match first, with the broad one as the explicit fallback. Two consequences worth designing for: - One id sharing broad keywords and a sharp intent regex shares one dedup budget — an early keyword hit silences the sharp moment. Split into two triggers when the sharp match must survive keyword noise. - The `dedup` field overrides the default per trigger: `never` (every match — pair only with sharp matchers), `{minutes: N}` (re-fires each bucket within a session — for long sessions that outlive their context). The schema rejects `dedup` on block triggers. ## Test before write — the dry-run loop No draft touches `vault.yaml` untested. Write the candidate entries to a temp file as a bare YAML list (exactly the entries, not nested under a scope key) and pipe synthetic events through the engine: ```bash cat > /tmp/triggers-draft.yaml <<'EOF' - id: release-protocol-reminder on: prompt enforce: inject keywords: [tagging, releasing] text: "Release protocol: ops-publish-kmd is the release chain." EOF ``` **Synthetic events** — one JSON object on stdin per run, always with `--explain`: ```bash # prompt event: {"session_id": "...", "prompt": "...", "cwd": "..."} printf '%s' '{"session_id":"dry-1","prompt":"thinking about releasing tomorrow","cwd":"/tmp"}' \ | kmd hook prompt --explain --triggers /tmp/triggers-draft.yaml # pretool event: {"session_id": "...", "tool_name": "...", "tool_input": {...}, "cwd": "..."} printf '%s' '{"session_id":"dry-2","tool_name":"Bash","tool_input":{"command":"git push --force origin main"},"cwd":"/tmp"}' \ | kmd hook pretool --explain --triggers /tmp/triggers-draft.yaml ``` **Reading the trace.** `--explain` prints one JSON object naming the resolved scope and, per trigger: the matcher verdict (`hit`, or which stage missed — `tool-miss`/`args-miss`/`files-miss`/`payload-miss`; prompt triggers carry `keywords`/`intent` evidence instead), the typed predicate verdict (`satisfied`/`unmet`/`vacuous`/`unknown`), the dedup verdict (`exempt`/`never`/`fresh`/`suppressed`), whether it fired, and the exact outcome the harness would receive. Diagnose a near-miss from the trace, never from empty output. Hook events always exit 0 — the trace is the only signal. **No state spent.** `--explain` never writes dedup state: probes repeat stably, and a probe never silences the trigger for a live session. A `dedup: "suppressed"` verdict means existing session state already carries the key — retest under a throwaway `session_id` (`dry-1`, `dry-2`, …) to see the fresh path. **Fire + near-miss discipline.** Every matcher runs at least one intended-fire case and one near-miss counterexample, and the user sees both results. The near-misses were written down while drafting the matcher — "prerelease" against `keywords: [releasing]` (word boundary holds), `echo "force push"` against the force-push regex (no match). A matcher proven only on its fire case is untested. **Predicate drafts.** A `when: newer-than` candidate is tested against the real vault filesystem read-only — the vault-root argument points at the real vault so the globs select real pages. Never fabricate retro or release fixtures inside a real vault to make a predicate evaluate. ## Write gate 1. Show the exact YAML block and its target key. 2. Write only on explicit approval — `vault.yaml` is a controlled-vocabulary surface. 3. Run `kmd validate` after the write. Red means revert before anything else: an invalid config fails the load for every tool, not just the new trigger.