* 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
This commit is contained in:
@@ -337,6 +337,7 @@
|
||||
"federated-config.cjs",
|
||||
"frontmatter.cjs",
|
||||
"gap-checker.cjs",
|
||||
"gate-predicate-evaluator.cjs",
|
||||
"git-base-branch.cjs",
|
||||
"graphify-command-router.cjs",
|
||||
"graphify.cjs",
|
||||
|
||||
158
docs/adr/2008-command-exit-zero-gate.md
Normal file
158
docs/adr/2008-command-exit-zero-gate.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# ADR-2008: Generic gate-predicate evaluator (`command-exit-zero`)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Status** | Accepted |
|
||||
| **Date** | 2026-07-04 |
|
||||
| **Issue** | [#2008 — No generic evaluator for third-party capability gates](https://github.com/open-gsd/gsd-core/issues/2008) |
|
||||
| **Supersedes** | — |
|
||||
| **Amends** | ADR-0894 (capability declaration format — `check.predicate` evaluation path) |
|
||||
|
||||
## Context
|
||||
|
||||
Capability gates are declared in `capability.json` under `gates[].check`. The
|
||||
registry validator (`capability-validator.cjs:validateGate`) recognises three
|
||||
mutually-exclusive `check` shapes — `query`, `predicate`, and `agentVerdict` —
|
||||
and the loop-resolver (`loop-resolver.cts:renderLoopHooks`) renders all active
|
||||
gates to the workflow gate-dispatch sites.
|
||||
|
||||
Prior to this ADR, only `check.query` was **enforced**: every workflow
|
||||
gate-dispatch site ran `gsd_run check ${hook.check.query}`, which routes through
|
||||
the fixed if-chain in `check-command-router.cts:routeCheckCommand`. A
|
||||
`check.predicate` (e.g. the `security` capability's
|
||||
`artifact-frontmatter-equals` declaration) was **declaration-only** — rendered
|
||||
for display, never evaluated. The security capability's `threats_open == 0`
|
||||
enforcement worked only because `ship.md` hard-codes a `capId == "security"`
|
||||
prose branch that reads `SECURITY.md` frontmatter directly. There was no
|
||||
extension path for a third-party capability's gate to actually fire.
|
||||
|
||||
Issue #2008 asked for a generic, data-driven gate-evaluation path. Two candidate
|
||||
directions were proposed:
|
||||
|
||||
1. A generic `check.predicate` evaluator covering `artifact-frontmatter-equals`
|
||||
and future kinds.
|
||||
2. A documented, sandboxed **`command-exit-zero`** gate kind — "run `<cmd>`,
|
||||
block on non-zero exit" — matching the git-hook / CI-runner enforcement
|
||||
shape.
|
||||
|
||||
The maintainer chose **Option 2** (issue comment, 2026-07-04).
|
||||
|
||||
## Decision
|
||||
|
||||
Add a **generic gate-predicate evaluation path** with one built-in kind,
|
||||
`command-exit-zero`, scoped as follows.
|
||||
|
||||
### Declaration shape
|
||||
|
||||
A capability declares a `command-exit-zero` gate under the existing
|
||||
`check.predicate` block (already a first-class, validator-accepted shape):
|
||||
|
||||
```json
|
||||
"gates": [
|
||||
{
|
||||
"point": "ship:pre",
|
||||
"check": {
|
||||
"predicate": {
|
||||
"kind": "command-exit-zero",
|
||||
"command": "node scripts/check.sh \"${PHASE_DIR}\"",
|
||||
"timeout": 30
|
||||
}
|
||||
},
|
||||
"when": "my_cap.enabled",
|
||||
"blocking": true,
|
||||
"onError": "halt"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`predicate.command` is required (non-empty string, ≤ 4096 chars).
|
||||
`predicate.timeout` is optional (positive finite number, seconds; default 30).
|
||||
|
||||
### Sandbox contract (the security core of this ADR)
|
||||
|
||||
| Axis | Value | Rationale |
|
||||
|---|---|---|
|
||||
| Interpreter | `sh -c` (via `shell-command-projection.execTool`) | Cross-platform with the runtime's existing bash dependency; one string, no argv array to author |
|
||||
| cwd | Project root (the runtime `cwd`) | Matches the user's working context; the same root existing `check.query` gates operate from |
|
||||
| Environment | Inherit process env | The command runs as the user, on the user's machine, in the project they are working on — no sandbox boundary is crossed vs. the user's own shell. Override via the command itself (`env VAR=x ...`) |
|
||||
| Timeout | Default 30s; overridable per-gate | Bounded execution is non-negotiable; an unbounded gate could hang the loop forever |
|
||||
| Interpolation | `${PHASE_NUMBER}`, `${PHASE_DIR}`, `${PHASE_REQ_IDS}` substituted from gate context; undefined → `''`; all other `${X}` left untouched for `sh` to interpret | Parity with the context existing `check.query` gates already receive |
|
||||
| Result mapping | exit 0 → `block:false`; non-zero → `block:true`; timeout (SIGTERM) → `block:true` (`timed_out`); `sh` missing (ENOENT, exit 127) → `block:true` | Fail-closed: every non-zero outcome blocks. A blocking gate with `block:true` halts per the existing two-step gate contract |
|
||||
| Output cap | stderr/stdout tail embedded in `message` trimmed to 2000 chars | Keeps the `GATE_RESULT` payload context-bounded |
|
||||
|
||||
### Evaluation path
|
||||
|
||||
- A new pure leaf module `src/gate-predicate-evaluator.cts` owns
|
||||
`evaluatePredicate(predicate, context, deps)`. It is fully deps-injected (the
|
||||
subprocess seam is `runBoundedShell`) — no fs, no child_process, no config —
|
||||
so it is trivially unit-testable without spawning. A `KIND_TABLE` dispatches
|
||||
by `predicate.kind`; adding a future kind is a one-line registration.
|
||||
- `check-command-router.cts` gains a `predicate` subcommand
|
||||
(`gsd_run check predicate --predicate '<json>' [--phase-dir …] [--phase-number …]
|
||||
[--phase-req-ids …] --raw`). It parses flags, builds the production deps
|
||||
(wrapping `execTool`), calls `evaluatePredicate`, and emits the standard
|
||||
`{ block, message, details? }` envelope via `output()`.
|
||||
- The three generic workflow gate-dispatch sites — `execute:wave:post`,
|
||||
`execute:post`, `plan:post` — now branch on the gate's `check` shape:
|
||||
`check.query` → existing `gsd_run check <query>` path; `check.predicate` →
|
||||
`gsd_run check predicate`. The two-step Step-1 (command-failure → `onError`)
|
||||
/ Step-2 (`block` → halt) contract is **unchanged** — the predicate path
|
||||
emits the same envelope and the same check-command-failure semantics.
|
||||
|
||||
### Fail-closed mapping for malformed predicates
|
||||
|
||||
`evaluatePredicate` **throws** for a malformed predicate (missing/non-string
|
||||
command, non-positive timeout, oversized command, unknown kind). The CLI
|
||||
wrapper maps a throw to `error()` (non-zero exit), which the workflow treats as
|
||||
a **Step-1 command failure** — routed per the gate's `onError` (`halt` or
|
||||
`skip`). This deliberately does **not** conflate an evaluator bug with a
|
||||
legitimate gate-block decision: a recognised predicate returns
|
||||
`{ block, … }`; an unrecognised one fails the check command.
|
||||
|
||||
## Trust model
|
||||
|
||||
Capabilities are opt-in installs (like npm packages): installing one already
|
||||
trusts it to ship skills, agents, and hooks that run arbitrary code.
|
||||
`command-exit-zero` is therefore **not a new trust boundary** — it is another
|
||||
code-execution path for already-trusted capabilities. Security does not rely on
|
||||
secrecy (Kerckhoffs): the command is declared in plain JSON, and safety comes
|
||||
from bounded timeout + fail-closed mapping + the opt-in install, not from
|
||||
hiding the mechanism.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive:** Third-party capability gates now actually fire. The path is
|
||||
generic; future predicate kinds (e.g. a revival of `artifact-frontmatter-equals`
|
||||
to retire the hard-coded `ship.md` security branch) register in `KIND_TABLE`
|
||||
without further workflow changes.
|
||||
- **Positive:** The evaluator is a pure leaf with injected I/O, matching the
|
||||
ADR-857 module decomposition and the repo's test conventions.
|
||||
- **Negative / documented limitation:** A command that backgrounds a child
|
||||
(`sleep 100 &`) can outlive the timeout kill of its direct `sh` parent —
|
||||
`execTool` kills the direct child on SIGTERM, not the whole process group.
|
||||
This is the same property the existing `check.query` gates have (they too can
|
||||
spawn long-running subprocesses). Full process-group kill is a future
|
||||
hardening, out of scope here; the threat is a malicious capability, which is
|
||||
already trusted.
|
||||
- **Negative:** `sh` must be present. On Windows without a POSIX shell
|
||||
(git-bash / WSL), `command-exit-zero` gates fail-closed with exit 127. This
|
||||
matches the runtime's existing bash dependency for workflows.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Implementing the `artifact-frontmatter-equals` kind (the maintainer chose
|
||||
Option 2 over Option 1; the kind table is structured for it but it is not
|
||||
registered).
|
||||
- Retiring the hard-coded `security` branch in `ship.md`.
|
||||
- `check.agentVerdict` evaluation (advisory, `blocking:false`-forced, separate
|
||||
concern).
|
||||
- Process-group kill on timeout.
|
||||
|
||||
## References
|
||||
|
||||
- Issue: [#2008](https://github.com/open-gsd/gsd-core/issues/2008)
|
||||
- Parent bundle: #2004
|
||||
- ADR-0894 (capability declaration format)
|
||||
- ADR-0857 (capability system / module decomposition)
|
||||
- `src/gate-predicate-evaluator.cts`, `src/check-command-router.cts`
|
||||
- Diátaxis docs: `docs/reference/gate-predicates.md`, `docs/how-to/command-exit-zero-gate.md`
|
||||
@@ -63,6 +63,7 @@ See **[CONTRIBUTING.md — "Proposing an ADR or PRD"](../../CONTRIBUTING.md#prop
|
||||
| [1593-skill-mapping-converter-methodology.md](1593-skill-mapping-converter-methodology.md) | Skill mapping & converter methodology across runtimes | Accepted |
|
||||
| [1769-state-md-transition-module.md](1769-state-md-transition-module.md) | STATE.md Transition Module — intent-based transitions over scattered RMW callbacks | Proposed |
|
||||
| [1817-state-md-rebuild-derivability-contract.md](1817-state-md-rebuild-derivability-contract.md) | STATE.md rebuild — derivability contract (capstone 11th transition) | Accepted |
|
||||
| [2008-command-exit-zero-gate.md](2008-command-exit-zero-gate.md) | Generic gate-predicate evaluator with a `command-exit-zero` kind (#2008) | Accepted |
|
||||
|
||||
## Seam map
|
||||
|
||||
|
||||
146
docs/how-to/command-exit-zero-gate.md
Normal file
146
docs/how-to/command-exit-zero-gate.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# How-to: add a `command-exit-zero` gate to a capability
|
||||
|
||||
> **Diátaxis quadrant:** How-To. A step-by-step recipe for a capability author
|
||||
> who wants a gate that runs a shell command and blocks the loop on non-zero
|
||||
> exit. For the full specification, see
|
||||
> [Reference: gate predicates](../reference/gate-predicates.md).
|
||||
|
||||
## When to use this
|
||||
|
||||
Use a `command-exit-zero` gate when your capability can be verified by an
|
||||
existing command-line check — a test runner, a linter, a custom validator, a
|
||||
git-hook-style probe. The gate runs your command at a loop point you choose and
|
||||
blocks the loop (advisory or hard) when it exits non-zero.
|
||||
|
||||
This is the generic extension path introduced in #2008 / ADR-2008. Before it,
|
||||
only built-in `check.query` gates could fire; a third-party capability's
|
||||
declared gate was display-only.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A capability with a `capability.json` (see ADR-0894).
|
||||
- A command that exits `0` on success and non-zero on failure, runnable via
|
||||
`sh`. On Windows, `sh` must be present (git-bash / WSL) — otherwise the gate
|
||||
fails closed with exit 127.
|
||||
|
||||
## Step 1 — author the command
|
||||
|
||||
Write a command that follows the **exit-0-on-success** contract. It receives
|
||||
the project root as its cwd and the inherited process environment.
|
||||
|
||||
```bash
|
||||
# Example: a bundled check script shipped with the capability
|
||||
node "${GSD_CAP_DIR}/checks/pre-ship.js"
|
||||
```
|
||||
|
||||
You may interpolate three loop-context placeholders directly into the command:
|
||||
|
||||
| Placeholder | Meaning |
|
||||
|---|---|
|
||||
| `${PHASE_NUMBER}` | the active phase number (e.g. `03`) |
|
||||
| `${PHASE_DIR}` | the active phase directory |
|
||||
| `${PHASE_REQ_IDS}` | the phase's requirement ids |
|
||||
|
||||
Anything else (`${HOME}`, `$VAR`, etc.) is left for `sh` to interpret against
|
||||
the inherited env.
|
||||
|
||||
## Step 2 — declare the gate
|
||||
|
||||
Add a `gates` entry to your `capability.json`. Pick the loop `point` (one of
|
||||
the 12 canonical points), set `blocking` and `onError`, and gate it on a
|
||||
config key with `when`:
|
||||
|
||||
```json
|
||||
{
|
||||
"gates": [
|
||||
{
|
||||
"point": "ship:pre",
|
||||
"check": {
|
||||
"predicate": {
|
||||
"kind": "command-exit-zero",
|
||||
"command": "node \"${GSD_CAP_DIR}/checks/pre-ship.js\" \"${PHASE_DIR}\"",
|
||||
"timeout": 30
|
||||
}
|
||||
},
|
||||
"when": "my_cap.enabled",
|
||||
"blocking": true,
|
||||
"onError": "halt"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Field rules (enforced by the evaluator):
|
||||
|
||||
- `predicate.command` — required, non-empty string, ≤ 4096 chars.
|
||||
- `predicate.timeout` — optional, positive finite number of seconds (default 30).
|
||||
- `predicate.kind` — must be exactly `"command-exit-zero"`.
|
||||
|
||||
## Step 3 — choose `blocking` and `onError`
|
||||
|
||||
| Field | Effect |
|
||||
|---|---|
|
||||
| `blocking: true` | A `block: true` result **halts** the loop at this point. |
|
||||
| `blocking: false` | Advisory — the gate prints its `message` and the loop continues. |
|
||||
| `onError: "halt"` | If the check command itself fails (malformed predicate, unknown kind, or a command that cannot be evaluated), halt. |
|
||||
| `onError: "skip"` | On a check-command failure, warn and continue (do not read `block`). |
|
||||
|
||||
> A non-zero exit of **your** command is a gate *result* (`block: true`), not a
|
||||
> check-command failure — `onError` does not apply to it. `onError` covers only
|
||||
> the case where the gate could not be evaluated at all.
|
||||
|
||||
## Step 4 — test the gate directly
|
||||
|
||||
You can run the evaluator standalone to verify your predicate before wiring it
|
||||
into a loop point:
|
||||
|
||||
```bash
|
||||
gsd_run check predicate \
|
||||
--predicate '{"kind":"command-exit-zero","command":"node checks/pre-ship.js \"${PHASE_DIR}\"","timeout":30}' \
|
||||
--phase-dir ".planning/phases/03-my-phase" \
|
||||
--phase-number "03" \
|
||||
--raw
|
||||
```
|
||||
|
||||
Expected output on success:
|
||||
|
||||
```json
|
||||
{ "block": false, "message": "command exited 0", "details": { "kind": "command-exit-zero", "exitCode": 0 } }
|
||||
```
|
||||
|
||||
Expected output when your command fails (e.g. exit 1):
|
||||
|
||||
```json
|
||||
{ "block": true, "message": "command exited 1: <stderr tail>", "details": { "kind": "command-exit-zero", "exitCode": 1, "signal": null } }
|
||||
```
|
||||
|
||||
## Step 5 — verify it fires in the loop
|
||||
|
||||
Once declared and the capability is active, the loop-resolver renders the gate
|
||||
at your chosen point. The workflow gate-dispatch (`execute:wave:post`,
|
||||
`execute:post`, `plan:post`, `ship:pre`, …) detects the `predicate` shape and
|
||||
dispatches the evaluator automatically — no further wiring needed.
|
||||
|
||||
```bash
|
||||
gsd_run loop render-hooks ship:pre --raw
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Backgrounded children escape the timeout.** `command: "sleep 100 &"` returns
|
||||
before the sleep finishes; the timeout kills the `sh` parent, not the
|
||||
backgrounded child. Keep your command foreground, or have it self-manage its
|
||||
children. (ADR-2008, documented limitation.)
|
||||
- **`sh` must be on PATH.** On Windows without git-bash/WSL, the gate fails
|
||||
closed (exit 127). This matches the runtime's existing bash dependency.
|
||||
- **Output is trimmed.** Only the last 2000 chars of stderr/stdout surface in
|
||||
the gate `message`. Emit a concise diagnostic; don't rely on grepping long
|
||||
output downstream.
|
||||
- **Env is inherited.** The command sees the full GSD process environment. Do
|
||||
not put secrets in the command string; read them from env or a file like any
|
||||
shell script.
|
||||
|
||||
## Related
|
||||
|
||||
- [Reference: gate predicates](../reference/gate-predicates.md)
|
||||
- [ADR-2008](../adr/2008-command-exit-zero-gate.md)
|
||||
117
docs/reference/gate-predicates.md
Normal file
117
docs/reference/gate-predicates.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 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](../how-to/command-exit-zero-gate.md).
|
||||
|
||||
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
|
||||
|
||||
```json
|
||||
"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:
|
||||
```bash
|
||||
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:
|
||||
```json
|
||||
{ "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.
|
||||
|
||||
## Related
|
||||
|
||||
- [ADR-2008](../adr/2008-command-exit-zero-gate.md) — full decision record.
|
||||
- [How-to: add a command-exit-zero gate](../how-to/command-exit-zero-gate.md).
|
||||
- ADR-0894 — capability declaration format.
|
||||
- `src/gate-predicate-evaluator.cts`, `src/check-command-router.cts`.
|
||||
Reference in New Issue
Block a user