ADR 0296: when delegates trigger conditions to the project under an explicit v1 execution boundary
Status: accepted; the executable escape hatch of ADR 0293, governed per ADR 0294
Context
§The structured trigger menu is closed and deterministic by design: selectors, an inverted conjunction, a size threshold, and two delta-shape predicates. Some real conditions live outside any closed menu ("the generated client drifted from its spec"), and discern already has one idiom for delegating knowledge it cannot have: [jobs] and [standards] hand a command to the shell and read a protocol from its output. An expression language inside trigger values would grow without bound and still miss cases; a model call is banned from the gate outright.
Decision
§when = "<command>" delegates the firing condition to the project, with a closed three-outcome protocol and a short fixed budget.
- Exit
0fires the trigger and exit10passes it. Every other exit and every lifecycle failure—invalid output, timeout, cancellation, spawn, input, or cleanup failure—is indeterminate and produces a structured drop. An indeterminatestopserves its question over the complete structurally admitted evidence; an indeterminateadviseremains non-blocking and discloses the lost predicate evidence. - The command may print
DISCERN_MATCH <path>lines — the protocol sibling ofDISCERN_METRIC, same whole-token anywhere-on-a-line convention, rest of the line as the path — to declare the subject paths precisely. Without them the subject falls back to the structural matched set, which reopens more coarsely; declared paths that are empty, absolute, or escape the root are dropped. With selectors present, the selectors pre-scope the diff and provide the default subject;whendecides the firing. - Execution funnels through the gate's job runner (capture environment,
discernself-resolution, process-tree cleanup) under a pinned 10-second budget — a pre-flight condition answers in seconds or it is not a pre-flight condition. There is deliberately nounlesscommand key: negation belongs to the structuredunless_changed, and one executable key keeps the surface small enough to audit. - The v1 boundary, stated rather than implied: the merge-base configuration governs the command TEXT, but the command runs in the candidate worktree, so scripts, dependencies, configuration, and interpreters it references resolve from that worktree. The policy identity proves where the text came from; it does not prove an executable dependency closure. Trusted policy execution is a later cross-cutting concern shared with
[standards], scope gates,[jobs], and generated commands — solving it for one key alone would be a false assurance.
Consequences
§- A condition the menu cannot express costs one small script instead of a schema extension. Its uncertainty cannot masquerade as a pass: stop mode asks for current judgment and advise mode records the non-blocking evidence loss.
- Firing through
whenis only as deterministic as the project's command; the structured predicates remain the reproducible core, and an indeterminate stop deliberately favors review over an unproved negative. - A branch can change what a governed command DOES by editing files it references — visible in the diff, priced into the boundary statement above, and closed only by the future trusted-execution concern.
Alternatives considered
§- An expression language in trigger values. Rejected: it grows toward a worse shell while staying weaker than one, and every operator added is surface every project pays for.
- Treat every
whenerror as either fire or pass. Rejected: both invent a predicate answer the command did not supply. The indeterminate outcome preserves the uncertainty while stop and advise modes retain their distinct enforcement roles. - Running the command from the merge-base's tree (pristine closure). Rejected for v1: materializing a historical tree per probe is heavy machinery for a boundary that trusted policy execution must eventually own coherently across every delegated command, not just this one.