Files
msd-core/docs/reference/gate-predicates.md
Tom Boucher 8de2ff9121 feat(#2008): generic command-exit-zero gate-predicate evaluator (#2011)
* feat(#2008): add generic command-exit-zero gate-predicate evaluator

Third-party capability gates declared via check.predicate were rendered for
display but never evaluated (only built-in check.query gates fired; the
security capability's gate worked solely via a hard-coded ship.md branch).

Add a generic, deps-injected gate-predicate evaluator (src/gate-predicate-evaluator.cts)
that dispatches by predicate.kind. Built-in kind: command-exit-zero — runs a
bounded sh -c command at the project root (via shell-command-projection.execTool),
inherits env, exit 0 => pass, non-zero => block, timeout => block, fail-closed.

Wire a 'check predicate' subcommand into check-command-router.cts and extend
the three generic workflow gate-dispatch sites (execute:wave:post, execute:post,
plan:post) to route check.predicate gates to the new evaluator. The two-step
gate contract (command-failure => onError; block => halt) is unchanged.

- src/gate-predicate-evaluator.cts: pure leaf, KIND_TABLE extensible
- src/check-command-router.cts: cmdCheckPredicate + buildPredicateDeps + parsePredicateFlags
- docs/adr/2008-*, docs/reference/gate-predicates.md, docs/how-to/command-exit-zero-gate.md
- tests: 38 unit + integration tests (exit mapping, timeout, interpolation,
  property-based bijection, malformed-predicate fail-closed, real subprocess e2e)

Closes #2008

* docs(#2008): backfill changeset pr number 2011
2026-07-05 14:04:29 -04:00

4.5 KiB

Gate predicates (reference)

Diátaxis quadrant: Reference. This is the canonical specification of the capability gate check.predicate evaluation path. For a step-by-step authoring guide, see How-to: add a command-exit-zero gate.

A capability gate's check block carries exactly one of three shapes (query, predicate, agentVerdict), enforced by the registry validator (capability-validator.cjs:validateGate). This page documents the predicate shape and the kinds the built-in evaluator recognises.

Declaration

"gates": [
  {
    "point": "<loop-point>",
    "check": {
      "predicate": {
        "kind": "<kind>",
        "<kind-specific fields>"
      }
    },
    "when": "<config-key>",
    "blocking": true,
    "onError": "halt"
  }
]

The gate envelope (point, when, blocking, onError) follows the standard contract documented in ADR-0894 (capability declaration format) and the Loop Host Contract glossary entry in CONTEXT.md. This page covers only check.predicate.

Evaluation path

  1. The loop-resolver (gsd-tools loop render-hooks <point>) renders the active gate hook (including its check.predicate declaration) to the workflow.
  2. The workflow gate-dispatch reads the hook in-context and, when the check shape is predicate, runs:
    gsd_run check predicate --predicate '<predicate JSON>' [--phase-dir …] [--phase-number …] [--phase-req-ids …] --raw
    
  3. check-command-router.cts:cmdCheckPredicate parses the predicate, builds the production subprocess binding, and calls gate-predicate-evaluator.cjs:evaluatePredicate, which dispatches by predicate.kind.
  4. The evaluator returns the standard gate envelope:
    { "block": <bool>, "message": "<string>", "details": { … } }
    
  5. The workflow applies the two-step gate contract unchanged:
    • Step 1 — if the check command itself failed (non-zero exit, e.g. a malformed predicate / unknown kind), route per onError (halt or skip).
    • Step 2 — if the command succeeded, a blocking: true gate halts on block: true; an advisory gate shows message and continues.

Built-in kinds

command-exit-zero

Runs a declared command in a bounded sh -c subprocess; exit 0 → pass, non-zero → block, timeout → block. See ADR-2008 for the full sandbox contract.

Field Type Required Default Notes
kind string yes — Must be "command-exit-zero"
command string yes — The shell command. Non-empty, ≤ 4096 chars
timeout number no 30 Positive finite number, seconds

Interpolation. Before execution, three placeholders are substituted from the gate context; all others are left untouched for sh to interpret:

Placeholder Source Workflow flag
${PHASE_NUMBER} the active phase number --phase-number
${PHASE_DIR} the active phase directory --phase-dir
${PHASE_REQ_IDS} the phase's requirement ids --phase-req-ids

An undefined placeholder interpolates to the empty string.

Sandbox. cwd = project root; env = inherited from the GSD process; killed (SIGTERM) on timeout. The command runs as the user, on the user's machine — there is no sandbox boundary vs. the user's own shell. See ADR-2008 "Trust model".

Result mapping.

Command outcome block message
exit 0 false command exited 0
exit N (non-zero) true command exited N: <stderr/stdout tail, ≤2000 chars>
timeout (SIGTERM) true command timed out after <s>s: <tail>
sh missing (ENOENT, exit 127) true command exited 127: sh: not found

Validation errors (throw → check-command failure → Step-1 / onError).

  • Missing, non-string, empty, or whitespace-only command.
  • command longer than 4096 chars.
  • timeout present but not a positive finite number.
  • Unknown kind.

Extensibility

The evaluator dispatches through a KIND_TABLE. Adding a new built-in kind is a one-line registration in gate-predicate-evaluator.cts — no workflow changes required, since the workflow dispatches any check.predicate to the same gsd_run check predicate subcommand.