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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
67
docs/reference/exit-codes.md
Normal file
67
docs/reference/exit-codes.md
Normal 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.
|
||||
Reference in New Issue
Block a user