Development record
abcd tells an agent what to do next, after a refusal and after a success
Summary
Delivers itd-2610301428378830. Every refusal gets a registered code, a
one-line hint and the next command to try. The --json error envelope
carries all three, and abcd explain <code> prints the code's explanation.
Every successful --json result names what usually comes next. Both are read
from tables held in code, and tests hold them whole. Codes follow
adr-2610301638558260:
a code is a short name, and once a release has carried it, it is never
renamed or reused.
This spec builds on spc-2610301431075964, which adds abcd explain and the
command facts. abcd build reads - needs: only within one spec, so the order
is stated here: no step of this spec starts before that spec's steps 1 to 3
have landed. Steps 1 and 6 extend explain and the facts, and every step
touches internal/surface/cli/cli.go, which that spec and
spc-2610301431131636 also change. The order across the three is: that spec's
step 1, then spc-2610301431131636, then that spec's steps 2 to 4, then this
spec.
Grounding, read on 30 October 2026:
- Every refusal leaves through one place.
cli.run(internal/surface/cli/cli.go) takes the errorroot.Execute()returns and reads its exit code fromexitError(or anything withExitCode()). It then scrubs paths, withholds operands, masks control characters and, under--json, writeserrorEnvelope{abcd: "error", error, exit_code}to stdout. Otherwise it writes oneabcd: <msg>line to stderr. Under--json, stdout is a stream of documents, and the refusal is always the last one. - The front door builds
exitErrorliterals in 64 files. Counted by literal: 432 at exit 2 (refused), 5 at exit 3 (held by someone else) and 60 at exit 1 (a fault). Another 17 take their code from a variable, such asexitError{Code: res.ExitCode}, so no reading of the source can tell what they are. Most read a core error and wrap it in a per-verb helper, such ascaptureRefusal,implementRefusal,landRefusal,memoryRefusalorchecklistRefusal. - Usage errors (an unknown command or flag, the wrong number of arguments, a
flag-group conflict, a missing required flag) are set to exit 2 tree-wide
by
markUsageErrorsExitTwo. On the hook plane,failOpenNoArgsandfailOpenFlagErrormove them to exit 1. - Successful
--jsonoutput goes throughrender(internal/surface/cli/cli.go) at about 220 call sites, plus 8 direct encoders. Each verb's result is its own struct. Several already carry a top-levelnextof their own:drain,buildandimplement(the loop's next action),intent drain,update news,lab,ahoy's setup report,siteand the board. Some verbs stream several documents, such as the build loop's dispatch. - Next moves exist today only for a record id (itd-121):
record.Describereturnsnext_moves.
Approach
- The refusal registry. A new core package,
internal/core/refusal, holdstype Code stringand one table of entries. Each entry has:- the code (
<verb>/<reason>, lower-case kebab-case, such ascapture/remedy-missing, orusage/…for the framework's refusals); - a summary of one line;
- an explanation of a few lines;
- a next-command template (
abcd capture "<text>" --remedy "<fix>"), or the explicit wordnonewhere no command can help (for example, "held by a peer: wait"); - an optional
retiredrelease tag. A core error carries a code by wrapping:refusal.Wrap(code, err)returns an error whoserefusal.CodeOf(err)finds the code througherrors.As. So a core package names the code where the refusal is decided, and the front door adds nothing.
- the code (
- The front door carries it out.
exitError's fields become unexported, and it gains a refusal code. It is built only through two constructors:refused(exit, ref, msg)for exit 2 or 3, whererefis a registered code;exitWith(exit, ref, msg)for a code known only at run time (the 17 variable-code sites). Itsrefargument is required, and it may berefusal.Noneonly when the exit turns out to be 1. An exit of 2 or 3 withrefusal.Noneis still returned, but carries the codeinternal/uncoded. The test-only check below fails on that code, and so does the registry test, because the code is never registered. A test parses the package and refuses any composite literal ofexitErroroutside the two constructors. Before rendering,runtakes the code from the error, or else fromrefusal.CodeOf(err), or else from the usage classifier (approach 3). Then:- The
--jsonenvelope gainscode,hintandnext.abcd,errorandexit_codestay exactly as they are, so a reader of today's envelope is not broken. - The text form adds one line after today's:
abcd: <hint> — try: <next> (abcd explain <code>). - A next command is filled from the refused call's own command path. It
is never filled from the caller's arguments, which
runwithholds today and goes on withholding.
- Usage errors. One classifier in
runreads cobra's and pflag's error types (and, where they have none, the wording thatwithholdOperandsalready parses) and maps each to ausage/…code:usage/unknown-command;usage/unknown-flag;usage/missing-argument;usage/extra-argument;usage/flag-conflict;usage/missing-required-flag;usage/bad-flag-value. The next command isabcd explain <the path that resolved>. On the hook plane, that path is a hidden command, whichexplainanswers marked(hidden)(spc-2610301431075964, approach 3).
- What counts as a refusal. It is an exit 2 or an exit 3: abcd judged
the call and declined it. An exit 1 is a fault: abcd could not do what it
meant to, such as a read that failed or a host that closed the pipe. A
fault is not coded by this spec. There is one exception: the hook plane's
usage refusals. They are usage refusals that the fail-open rule moves from
exit 2 to exit 1, so they carry their
usage/…code at exit 1. Where one of today's other exit-1 literals is really a refusal, it moves to exit 2 with a code, and the step that moves it names it in its pull request. - The gates.
- Every code that is raised is registered: the literal test from
approach 2, plus a test over
internal/corethat everyrefusal.Wrapnames a registered constant. - Every refusal carries a code: a test-only check in
runfails the test binary when an exit 2 or 3 leaves without one. The tree-wide refusal tests that already run every verb's usage and refusal paths (for exampleTestEveryParentRefusesAnUnknownSubverband the per-verb refusal tests) exercise it. - Every code resolves: each registered code is answered by
abcd explain <code>, and each next template names a command path the tree has. - No two entries share a code: a uniqueness test over the registry, run in the merge queue like every test. This is the test the ADR names for two branches that pick the same name.
- No code is renamed or reused:
cmd/refusal-codes-syncwrites.abcd/development/release/refusal-codes.jsonfrom the registry, and a drift test holds it. The release guard (internal/core/changelog/guard.go, besidesurfaceSnapshotAt) reads the ledger at the last tag the way it reads the surface snapshot, and refuses a cut in which a code that ledger carries is missing (a retired code is still present). The code list never shrinks. - While the migration runs,
.abcd/development/release/refusal-uncoded.txtlists the files whose literals are not yet coded. The literal test exempts only those files, and the list may only shrink. The last step deletes the file and its exemption. The intent cannot ship while the file exists, and the close's fidelity review is told to check for it.
- Every code that is raised is registered: the literal test from
approach 2, plus a test over
abcd explain <code>.explaintakes a code wherever it takes a command path: an argument containing a/is a code. It prints:- the summary;
- the explanation;
- the next command;
- the command that raises the code;
- for a retired code, the tag it was retired in.
--jsonprints the entry. An unknown code is refused asexplain/unknown-code, naming the nearest code.
- Next steps on success. The command facts of spc-2610301431075964 gain
a
nextlist per visible command, held in one table (internal/core/surface/next.go). Each item is a command template and one line saying when it applies, such asabcd capture resolve <iss-N>with "once the fix lands". An entry may be the explicit empty list with a reason, such asabcd version: "nothing usually follows". That is how such a command names what comes next, and the reason is shown byexplain. Under--json, a successful command's result gets a top-levelnext_stepskey carrying the list. It is added insiderender, where the document is written, sorun, the verbs' structs and the byte order of every existing key stay exactly as they are:- A
PersistentPreRunEon the root wraps the command's stdout in a writer that carries the command path.render, given that writer, looks up the path'snextlist in the facts. renderencodes the result to bytes as today. When the document is a JSON object, it splices,"next_steps":[…]in before the final}. There is no decode and re-encode, so no key moves and no number changes.- The key is
next_stepseverywhere, nevernext, because many verbs already usenextfor something else (the grounding lists them). So no existing key is replaced or shadowed. A test fails if a result already carries a top-levelnext_steps, which is checked on the encoded bytes rather than kept as a hand-written list. - A command whose facts mark it as streaming (the build loop's dispatch,
history drain) gets the key only on the document it writes throughrenderFinal, its closing summary. Every other command renders one document, and that document gets the key. - The 8 direct encoders move onto
render, or ontorenderFinalfor a streaming summary. - The hook plane (hidden commands) is left alone.
- A command whose
--jsondocument is not an object (a bare array) cannot take a key without breaking its readers. Step 6 first lists every such command. If it finds any, the step stops and the product thinker is asked how such a command should name its next step. Until then, nothing is wrapped. The facts table is the one source, soexplain, the per-command JSON and the page block show the same next steps that a result carries.
- A
How each criterion is met
- Every refusal carries a code, a hint and the next command, and the code
resolves. Approaches 1 to 6. Tests:
- the literal test, the constructor rule and the
refusal.Wraptest, so every refusal raised in code names a registered code, including the variable-code sites; - the uniqueness test over the registry;
- the test-only
runcheck over the tree-wide refusal tests, so no exit 2 or 3 leaves without a code; - an envelope test, where a refused
capturewith no--remedyhascode: capture/remedy-missing, a hint, andnextnamingabcd capture; - an
explaintest over every registered code; - the next-template test;
- the ledger drift test, and a release-guard test that removes a code from a fixture ledger and is refused.
- the literal test, the constructor rule and the
- Every successful result names what usually comes next, and each step is
a real command. Approach 7. Tests:
- a tree-wide test that every visible command has a
nextentry; - every template names a command path the tree has;
- a successful
--jsonrun of a fixture verb carries that verb'snext_steps, with every other byte of the document unchanged; - a verb whose own result already has
nextkeeps it as it was; - a streaming verb carries
next_stepsonly on its closing summary.
- a tree-wide test that every visible command has a
Out of scope: exit-1 faults (approach 4); the person's what-next menu under
the board (itd-2610031215002409), which reads the same facts once both exist;
next_moves for a record id (itd-121), which stays as it is.
Footprint
- packages: internal/core/refusal, internal/core/surface/next.go, internal/surface/cli/cli.go, internal/surface/cli/usage_errors.go, internal/surface/cli/explain.go, internal/surface/cli, internal/core, cmd/refusal-codes-sync, internal/core/changelog, .abcd/development/release/refusal-codes.json, .abcd/development/release/refusal-uncoded.txt, commands/explain.md, .abcd/development/brief/04-surfaces/41-explain.md
- tests: internal/core/refusal/refusal_test.go, internal/surface/cli/refusal_codes_test.go, internal/surface/cli/refusal_envelope_test.go, internal/surface/cli/next_steps_test.go, internal/core/changelog/refusal_ledger_test.go
Steps
- The registry, the envelope,
explain <code>, the ledger and the shrinking exemption list- needs: none
- packages: internal/core/refusal, internal/surface/cli/cli.go, internal/surface/cli/explain.go, cmd/refusal-codes-sync, internal/core/changelog, .abcd/development/release/refusal-codes.json, .abcd/development/release/refusal-uncoded.txt, commands/explain.md, .abcd/development/brief/04-surfaces/41-explain.md
- tests: internal/core/refusal/refusal_test.go, internal/surface/cli/refusal_codes_test.go, internal/surface/cli/refusal_envelope_test.go, internal/core/changelog/refusal_ledger_test.go
- Usage errors on every command, hook plane included
- needs: 1
- packages: internal/surface/cli/cli.go, internal/surface/cli/usage_errors.go
- tests: internal/surface/cli/refusal_codes_test.go
- The record verbs' refusals: capture, intent, spec, decide, ideate, reserve, announce, peers, status, embargo and banlist
- needs: 1
- packages: internal/surface/cli, internal/core/capture, internal/core/intent, internal/core/spec, internal/core/ideate, internal/core/claim
- tests: internal/surface/cli/refusal_codes_test.go
- The delivery verbs' refusals: build, implement, drain, land, launch, ship, checklist and docs
- needs: 1
- packages: internal/surface/cli, internal/core/implement, internal/core/landing, internal/core/launch, internal/core/release
- tests: internal/surface/cli/refusal_codes_test.go
- The remaining verbs' refusals, and the exemption list deleted
- needs: 2, 3, 4
- packages: internal/surface/cli, internal/core, .abcd/development/release/refusal-uncoded.txt
- tests: internal/surface/cli/refusal_codes_test.go
- Next steps on success
- needs: 1
- packages: internal/core/surface/next.go, internal/surface/cli/cli.go, internal/surface/cli/explain.go
- tests: internal/surface/cli/next_steps_test.go
Frontmatter
| id | spc-2610301431108175 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610301431108175-when-abcd-refuses-an-agent-s-command-the.md
open on GitHub ↗ · commit history ↗
References1
- itd-2610301428378830abcd tells an agent what to do next, after a refusal and after a successimplements
mentions5
- adr-2610301638558260A published refusal code is never renamed or reused
- itd-121Type The Id, Get Your Next Move
- itd-2610031215002409A 'what next?' menu under the board offers the next step, in a Terminal and in a Claude Code session
- spc-2610301431075964An agent learns how to call abcd from the abcd it runs
- spc-2610301431131636An agent can practise a filled-in form before it counts