enhance(#3913): docs, and the guards come down (#3994)

ADR-3889 terminal phase. Generated docs/reference/exit-codes.md from the exit-code declaration with a --check drift arm; deleted the inert soft-error-exit-zero oracle; promoted untyped-success from SMELL to VIOLATION so it can fail a build; pruned all 5 smell-baseline entries.

Fixed inline: two mis-scoped oracles (routing-validity, value-hygiene), a second source behind the band table, unescaped declaration strings reaching Markdown, and a pre-existing Windows 8.3 short-name path-comparison defect.

Guard ledger corrected from a claimed net -4 to a measured net -1.

Closes #3913
This commit is contained in:
Tom Boucher
2026-08-28 13:10:20 -04:00
committed by GitHub
parent c3e667df34
commit 12f9d1d9a0
16 changed files with 1517 additions and 152 deletions

View File

@@ -95,6 +95,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
- [Review and verification capabilities](reference/review-verification-capabilities.md) — code review, security, and Nyquist capability ownership and hook contracts
- [Gate predicates](reference/gate-predicates.md) — canonical specification of the phase-gate predicate vocabulary
- [Capability matrix](reference/capability-matrix.md) — generated catalogue of every capability's role, tier, extension points, hook kinds, and `engines.gsd`
- [Exit code reference](reference/exit-codes.md) — generated catalogue of every registered process exit code, its name, meaning, and owning module, plus the reserved bands and the v1/v2 exit contract
- [Capability manifest](reference/capability-manifest.md) — the full `capability.json` schema and validation rules
- [`gsd capability` command](reference/gsd-capability-command.md) — install / update / remove / list reference for third-party capabilities
- [Workflow fragments](reference/workflow-fragments.md) — in-file `<!-- gsd:section -->` marker grammar for fragmentizing workflow markdown at emission time

View File

@@ -273,6 +273,56 @@ and it is the property ADR-2980's declined Option 3 lacked.
`n/no-process-exit: 'off'` exemption block, and the duplicated `scripts/lib/cli-exit.cjs`;
**added** one rule (`local/require-registered-exit`) plus the registry's own `--check`. Net **−2**.
> **Amendment — 2026-08-28: the ledger above is superseded (#3913).**
>
> Three of its terms did not survive contact with the code, and the net it states was never
> achievable. It is left in place unedited so the drift is legible; the measured ledger is below.
>
> 1. **`scripts/lib/cli-exit.cjs` was not deleted.** Phase 0 changed it from a duplicated module
> into a *generated* one, so the line retires a copy that still exists. The epic body was
> corrected to net **−1** at the time; this ADR's line was not.
> 2. **"their four baseline entries" was five** — four `soft-error-exit-zero` and one
> `untyped-success`, counted directly off `tests/qa/smell-baseline.json`. Issue #3913 repeated
> the figure of four, and so did the first recon pass of this phase. This is the second
> documented count in this epic to drift, after [ADR-2980](2980-payload-carried-error-is-a-degraded-result.md)'s
> 60-versus-64 `output({error})` sites.
> 3. **Only one of the two oracles was retired.** `soft-error-exit-zero` is deleted: its condition
> is now *declared* by the exit contract (`output({error})` → `DEGRADED`, 0 under v1 and 80
> under v2), so an inert SMELL restating it is ledger inflation. `untyped-success` is **not**
> deleted — it asserts that a `KIND.PROSE` command exposes no typed surface, which has nothing
> to do with exit codes, and neither the registry nor `local/require-registered-exit` replaces
> it. The clause this ADR and #3913 both rely on — *"the registry and the ESLint rule enforce
> the same property by construction"* — is simply false for it. Rather than drop the property,
> it was **promoted from SMELL to VIOLATION**, so it can now fail a build, which it never could
> before (`runOracles`'s `get failed()` returns `violations` only).
>
> **Measured ledger for the epic as delivered:** −1 oracle (`soft-error-exit-zero`), −1
> `n/no-process-exit: 'off'` exemption block, +1 rule (`local/require-registered-exit`), +1 registry
> `--check`, +1 generated-docs `--check` (`docs/reference/exit-codes.md`). One further guard changed
> strength rather than count: `untyped-success` SMELL → VIOLATION. Two mis-scoped oracles were
> corrected in passing (`routing-validity`, `value-hygiene`) — see #3913.
>
> **Net −1 by count**, not −4.
>
> The −4 first written here counted the five pruned `smell-baseline.json` entries in the same units
> as oracles and lint rules. They are not guards — they are *acknowledgements* that a guard fired.
> Removing them strengthens the surface rather than removing anything from it, and folding them into
> the count inflates the negative by five. An ADR about honest accounting should not pad its own
> ledger, so the entries are recorded as what they are and excluded from the count.
>
> **Two limits on how strong this is, stated rather than implied:**
>
> - **`soft-error-exit-zero`'s condition is now declared, not enforced.** No oracle references
> `KIND.SOFT_ERROR` after its deletion, and the corpus still contains four soft-error steps that
> nothing observes. Under `v1` — the default — those sites still exit `0`. The condition is
> modelled in the contract and projects to `80` only under the opt-in `v2`. That is a deliberate
> trade, not an equivalent replacement, and "the registry enforces the same property" would be too
> strong a claim for it.
> - **`untyped-success` is promoted against a corpus that no longer exercises the case.** Its
> `KIND.PROSE` count reached 0 because the sole prose-producing step was changed to
> `smart-entry --json`. The promotion is real and the guard now fails a build — but what it
> currently guards is that no *new* prose-only step appears, not that an existing one is caught.
## Revisit if
- Domain allocations exceed roughly a dozen. That means the generic four are wrong, not that the

View File

@@ -0,0 +1,67 @@
# Exit code reference
> **Generated file — do not edit by hand.**
> This page is generated from the exit-code declaration
> (`gsd-core/bin/shared/exit-codes.json`) by `scripts/gen-exit-code-docs.cjs`
> and kept honest by a drift guard in `npm run lint:generated-sync` (which runs
> `node scripts/gen-exit-code-docs.cjs --check`). Any manual edit is overwritten
> on the next generation run. To register a new code, add an entry to the
> declaration and run `node scripts/gen-exit-code-registry.cjs --write && node
> scripts/gen-exit-code-docs.cjs --write`.
See also: [ADR-3889 — one exit-code registry](../adr/3889-process-exit-contract.md) —
[Adopt the v2 exit contract](../how-to/adopt-the-v2-exit-contract.md) —
[JSON error mode](../json-errors.md)
---
## Registered codes (6)
Every process-level exit code `gsd-tools`, its hooks, and its scripts may terminate
with, by name, meaning, and the module that owns it.
| code | name | meaning | owning module | authorized by |
|---|---|---|---|---|
| 2 | `HOOK_DENY` | Hook protocol deny — the harness blocks the tool call | `hook-adapter` | ADR-3889 |
| 64 | `USAGE` | Caller error — bad argv, unknown subcommand, missing argument | `generic` | ADR-3889 |
| 66 | `NO_INPUT` | Ran; zero units were in scope, and that emptiness is known to be genuine | `generic` | ADR-3889 |
| 69 | `UNAVAILABLE` | Could not run — prerequisite absent, input unreadable, scope unestablished | `generic` | ADR-3889 |
| 70 | `INTERNAL` | Self-failure — crash, timeout, killed subprocess | `generic` | ADR-3889 |
| 80 | `DEGRADED` | Ran to completion and is reporting a condition through its result payload rather than as a process failure | `gsd-tools` | ADR-3889 + ADR-2980 |
---
## Reserved bands
The registered codes above are not chosen freely — each falls inside one of a
fixed set of allocatable bands (ADR-3889 §1). A code outside these bands can
never be registered; validation rejects it before it reaches the tables above.
This is what makes an unfamiliar number in a CI log actionable: look up its
band first, then its registered name if it has one.
| Band | Meaning |
|---|---|
| `0`–`1` | **Free — never allocatable.** `0` is the universal "succeeded" convention and `1` is the universal "failed, no further detail" convention across nearly every CLI ecosystem. Registering either here would collide with that universal meaning instead of adding a distinct, named signal — so the registry leaves both permanently unallocated. |
| `2` | Reserved exclusively to the Claude Code hook-protocol deny (`HOOK_DENY`) — owned by `hook-adapter` and no other module. |
| `3`–`13` | **Node-reserved.** Node.js itself assigns meaning to this range (e.g. internal JavaScript errors, fatal exceptions, invalid argument errors) before a GSD process ever gets a chance to project its own outcome. Allocating one of these would be indistinguishable from a Node-level failure the process never intended to report. |
| `14`–`63`, `79`, `126+` | Outside every allocatable band — not Node-reserved, but also not opened for GSD use. `126`+ additionally collides with the shell convention for "command not executable" / "signal N" (`128+N`), which a process exit code must never impersonate. |
| `64`–`78` | **Generic band.** Codes any module may use for caller-facing, non-domain-specific outcomes (bad argv, no input in scope, a missing prerequisite, an internal crash). |
| `80`–`125` | **Domain band.** Codes reserved for a specific product surface's own vocabulary — currently only `gsd-tools`' `DEGRADED` (a completed run reporting a condition through its payload rather than as a process failure). |
## The v1/v2 exit contract
ADR-3889 §4 adds a **version projection** on top of this registry, not a second
registry: every registered name above projects to the *same* code under both
contract versions, with one deliberate exception — `DEGRADED`. Under the
default, backward-compatible `v1` contract, a payload-carried error
(`output({error})`) still exits `0`, exactly as
[ADR-2980](../adr/2980-payload-carried-error-is-a-degraded-result.md) ratified
for the pre-existing call sites that already depended on that behavior — **64**
call sites across 9 modules per that ADR's own amendment (its original text
said ~60). Under the
opt-in `v2` contract, the same outcome exits `80` (`DEGRADED`) instead, so a
caller that wants to branch on the exit code alone — without parsing stdout —
can opt in without breaking every existing consumer. See
[Adopt the v2 exit contract](../how-to/adopt-the-v2-exit-contract.md) for how to
turn this on, and [JSON error mode](../json-errors.md) for the full fault vs.
degraded-result channel taxonomy this registry sits underneath.