Development record
An agent learns how to call abcd from the abcd it runs
Summary
Delivers itd-2610301346085246. A new verb, abcd explain <verb> [<sub-verb>…],
prints one command's facts in at most 40 lines. abcd explain <path> --input <flag> prints the exact shape a filled-in input must take, at the schema
version this binary accepts, with one example that passes the command's own
decoder. Those facts are gathered once, from the program's own code. The same
facts are written to a plain JSON file per command and to a generated block in
each command page. A drift test fails the build when any copy is stale, and a
lint fails when a page's hand-written prose names a flag, command or schema
version that the facts do not hold. Every page's first instruction sends the
agent to abcd explain before it fills in an input.
Build order across the three sibling specs. All three touch
internal/surface/cli/cli.go, the owning core packages and the command pages,
and abcd build refuses two runs whose footprints overlap. So they are built
one after another, in this order:
- this spec's step 1;
- spc-2610301431131636 (the practice run);
- this spec's steps 2 to 4;
- spc-2610301431108175 (refusal codes and next steps), which needs
explain.
Grounding, read on 30 October 2026:
- Most command facts already have one home in code, and the front door reads
them from there:
- the cobra tree, which
commandSurface(internal/surface/cli/surface.go) walks into[]surface.Commandfor the committed snapshot.abcd/development/release/surface.json(schema 4: commands, flags, whether a flag is required, help placement, sentence, successor); - each command's one sentence (
internal/core/surface/sentences.go), whichapplySentencessets as cobra's Short and writes into each page'sdescription:; - one worked example per command with a required input
(
internal/core/surface/examples.go), set as cobra's Example; - closed sets of flag values, which
enumHelp(internal/surface/cli/cli.go) renders into help from the same slice the reader checks against.
- the cobra tree, which
- What has no home in code is the shape of each filled-in input. The shape is
a Go type in the owning core package, decoded through
jsonstrict.Decodeor a local strict decoder. Its version is a constant there, such asideate.SchemaVersion = 2,release.ChangelogSchemaVersion = 2,scribe.SchemaVersion = 1or the lifeboat synthesis versions. Its example is written only in the page's prose, and nothing checks it. - The tree registers these filled-in inputs (a flag ending
-jsonthat the command reads):ideate record --verdict-json;intent prepass --findings-json;intent consistency ingest --findings-json;- the intent audit ingest
--verdict-json; docs fidelity record --verdict-json;launch ship --changelog-json;reading ingest --reading-json;scribe ingest --scribe-json;- the four disembark synthesis inputs (
--lessons-json,--principles-json,--press-release-json,--review-json); library ingest --pages-jsonandlibrary ask --page-json.
- Two generated page blocks already set the pattern this spec repeats:
- the asking-rules block in
commands/intent.md, between<!-- generated: asking-rules -->and<!-- /generated -->, written bycmd/asking-syncand gated byTestIntentPageAskingBlockIsGenerated; - the brief's surface appendix (
internal/core/surface/appendix.go), which is composed from the same[]surface.Commandas the snapshot.
- the asking-rules block in
docs/reference/cli/commands.mdis written bycmd/abcd-gen-cli-reffromcli.GenerateReferenceand gated byreference_test.go.TestCommandPagesMatchTheMenu(internal/surface/cli/menu_test.go) already holds pages to the menu.
Approach
- The command facts. A
surface.Factsvalue per command. The facts come from the same walk of the cobra tree as the snapshot:commandSurface(internal/surface/cli/surface.go) is extended to return both values from one pass. The snapshot'sCommandandFlagkeep their schema-4 shape, so the release guard's comparison with the last tag (changelog.GuardSurface) is untouched. The facts carry what the snapshot leaves out: defaults, allowed values, the usage line, the example and the inputs. Each value holds:- the path and the one sentence;
- the usage line;
- each flag's name, type (bool, string, int, list), default, whether it is required, and its allowed values when it takes a closed set;
- the filled-in inputs the command reads;
- the worked example.
A closed set reaches the walk through a cobra flag annotation, set by one
helper (
enumFlag) that also writes the help text throughenumHelp. So the help, the reader's check and the facts all read one slice. Every flag whose help today callsenumHelpmoves to the helper. A test holds each annotation equal to the slice its reader checks against.
- The input shapes. A registry in the front door
(
internal/surface/cli/inputs.go) names, for each filled-in input flag:- the command path and the flag;
- the owning package's Go type and schema-version constant;
- an example, held as a constant beside the type in the owning core package
(for instance
ideate.Example), never in the front door; - the owning package's parse-and-validate function, the one its writing command calls, run without writing. Six inputs carry no schema version today:
intent audit ingest --verdict-json;intent prepass --findings-json;intent consistency ingest --findings-json;docs fidelity record --verdict-json;library ingest --pages-json;library ask --page-json. Each gets a version constant of 1 in its owning package. For an object input, the decoder reads an absentschema_versionas 1, so every caller that works today keeps working, and it refuses any other version. One input is not an object:library ingest --pages-jsonis a bare array ([]DistilledPage). Its entry declares an array root, and its version is the registry's alone.explainprints "schema_version 1 (the input carries none)" for it.surfacederives a field list from the Go type by reflection, starting from the declared root (an object's fields, or an array's element): each JSON name, its type, whether it is required (noomitempty), and a nested object's fields at most four levels deep. Field docs come from adoc:"…"struct tag where the owning package writes one. The registry is held whole by a test: every visible flag ending-jsonthat the command reads is in it, and every entry names a real command and flag. So a new input cannot ship without its shape, and an output-only flag is declared as such once, in the same table.
abcd explain. A new top-level verb in the agents block.abcd explain <path…>prints, in this order, at most 40 lines:- the sentence;
- the usage line;
- one line per flag (
--name <type> [one of a | b] (required)); - one line per input (
--verdict-json: ideate verdict, schema_version 2; abcd explain ideate record --input verdict-json); - the example;
- one line naming the sub-commands, when there are any.
- A command whose flags would run past 40 lines prints the first lines and
then one line saying how many more there are and how to get them
(
--json). The test holds the bound for every visible command. --input <flag>prints that input's schema version, its field list and its example.--jsonprints the facts document. It is the same document as the command's committed JSON file (approach 4), cut to the one command.- A hidden command (the hook plane) is answered too, marked
(hidden)on its first line, so a refusal on that plane can nameexplainas its next step. A path the tree does not have is refused (exit 2), with the nearest real path named, the way the staleness note reads paths today. A bareabcd explainprints its own help. explaingets its sentence, its example, its help placement, a plugin page (commands/explain.md), a brief chapter (04-surfaces/41-explain.md) with its register row, and the regenerated snapshot and CLI reference, so it is wired on both surfaces.
- The derived copies. One generator,
cmd/command-facts-sync, run bymake command-facts-sync, writes two things from the facts:docs/reference/cli/commands/<verb>.json: one file per top-level verb, holding that verb's whole subtree. It sits beside the generated CLI reference, ships in every checkout and in the plugin, and is not read by any host as a command page. The file has aformat_version(starting at- and the schema version of each input. It carries no binary version, so it changes only when a fact changes.
- A block in each
commands/<verb>.md, between<!-- generated: command-facts -->and<!-- /generated -->, holding:- for each sub-command, its usage line; - its inputs, each with its schema version; - its example. The block's first line names its format version and says the running binary's
abcd explainoutranks it. The block goes at the end of the page, so the hand-written guidance reads first. A verb with no flags and no inputs gets a one-line block. A visible verb with no plugin page (rules,specandstatuslinetoday) gets its JSON file but no page and no block. Which verbs have a page stays the menu test's rule (TestCommandPagesMatchTheMenu), so the generator never creates a page. Drift is gated the way the asking block is: one test per copy fails, naming the file, when the committed bytes differ from a fresh rendering. Both tests are in thePRECHECK_AUDIT_TESTSset, somake precheckandmake preflightboth run them.
- The prose lint. A pure function in
internal/core/lint(CommandPageFacts) reads each page outside its generated blocks, given the facts, and reports a finding with the page and line for each of:- an abcd invocation (a fenced line, or a backticked span, starting with
the binary or
abcd) whose command path is not a command the tree has, hidden commands included; - a
--flagin such an invocation that the resolved command, or a persistent flag on its path, does not register; - a
"schema_version": Nin a JSON example that is next to an input's flag, where N is not that input's version. A line carrying<!-- facts-lint: historical -->is exempt. That is for prose that names an old version on purpose, such ascommands/ideate.mdsaying that version 1 is refused. A test ininternal/surface/cliruns the function over the committed pages and fails on any finding. The step that adds the lint also fixes, or marks, every finding it reports on the pages as they stand.
- an abcd invocation (a fenced line, or a backticked span, starting with
the binary or
- The page's first instruction. A second generated block,
<!-- generated: ask-the-binary -->, stands straight after each page's H1 heading. It holds one sentence: before filling in an input for this command, runabcd explain <verb>(spelled with the page's own binary rung); where that answer and this page disagree, follow the binary, since the page may be older. The same generator writes it, and a test holds it as the first content after the heading on every page incommands/. A page has no other first instruction.
Decisions made in this spec
Neither decision was put to the product thinker, because each answers a mechanism question that the intent's criteria leave open:
- The per-command JSON lives in
docs/reference/cli/commands/, not beside the pages incommands/. Host loaders read that folder as command pages, so an extra file type there is a risk with no gain. The CLI reference is already the generated, user-facing copy of the tree. - The page blocks carry each input's schema version and a format version, not the binary's version. A binary version changes at every release and in every dev build, and would make every page drift at each cut. The version an agent needs is the input's. This is the "version on the generated page blocks" that the intent's decision 6 claims over itd-111.
How each criterion is met
explainin at most 40 lines, from the one source. Approaches 1 and 3. Tests:- a tree-wide test runs
explainon every visible command and sub-command, holding each to at most 40 lines; - each output's flags, inputs and example equal the command's facts;
- a hidden path and a misspelt path are refused with exit 2.
- a tree-wide test runs
- An input's shape and an example that passes the decoder. Approach 2.
Tests:
- for every registry entry,
explain … --input <flag> --jsonnames the owning package's schema-version constant; - its example passes the owning package's parse-and-validate function;
- a registry test refuses a visible reading
-jsonflag with no entry.
- for every registry entry,
- One build, three copies, preflight fails on drift. Approaches 1, 3 and 4.
Tests:
- the JSON-file drift test and the page-block drift test;
- a test that
explain --jsonfor a command is byte-equal to that command's slice of its committed JSON file.
- The prose lint names page and line. Approach 5. Tests:
- fixtures with a flag the command lacks, a path the tree lacks, and a
stale
"schema_version", each reported with its line; - the historical marker suppresses the finding;
- the committed pages report none.
- fixtures with a flag the command lacks, a path the tree lacks, and a
stale
- Each page's first instruction is
abcd explain. Approach 6. Test: everycommands/*.mdhas the ask-the-binary block as its first content after the H1, and the block names that page's verb.
Out of scope, recorded elsewhere:
- refusal codes and next steps (itd-2610301428378830);
- the practice run (itd-2610301428390406);
- the root help's
--jsonverb listing, which will read these facts (iss-2610301320457709); - the help's screen layout, including the agents block's
(read commands/<verb>.md)pointer (itd-2610301327282266).
The --answers files that interviews read are not -json flags. They are
answers to questions, and their shape is already printed by the interview
that asks them, so they stay outside the registry. An answers file is listed
as an input in explain by name only.
Footprint
- packages: internal/core/surface/facts.go, internal/core/surface/inputs.go, internal/core/surface/sentences.go, internal/core/surface/examples.go, internal/surface/cli/explain.go, internal/surface/cli/inputs.go, internal/surface/cli/surface.go, internal/surface/cli/cli.go, internal/surface/cli/helpgroups.go, internal/core/lint/commandpagefacts.go, internal/core/ideate, internal/core/intent, internal/core/release, internal/core/reading, internal/core/scribe, internal/core/lifeboat, internal/core/docfidelity, internal/core/memory, cmd/command-facts-sync, Makefile, commands, docs/reference/cli, .abcd/development/release/surface.json, .abcd/development/brief/04-surfaces/41-explain.md, .abcd/development/brief/04-surfaces/README.md
- tests: internal/core/surface/facts_test.go, internal/core/surface/inputs_test.go, internal/surface/cli/explain_test.go, internal/surface/cli/inputs_test.go, internal/surface/cli/commandfacts_drift_test.go, internal/surface/cli/commandpage_lint_test.go, internal/core/lint/commandpagefacts_test.go, cmd/command-facts-sync/main_test.go
Steps
- The command facts and the input registry
- needs: none
- packages: internal/core/surface/facts.go, internal/core/surface/inputs.go, internal/surface/cli/inputs.go, internal/surface/cli/surface.go, internal/surface/cli/cli.go, internal/core/ideate, internal/core/intent, internal/core/release, internal/core/reading, internal/core/scribe, internal/core/lifeboat, internal/core/docfidelity, internal/core/memory
- tests: internal/core/surface/facts_test.go, internal/core/surface/inputs_test.go, internal/surface/cli/inputs_test.go
- landed: caeab6619329
- The explain verb on both surfaces
- needs: 1
- packages: internal/surface/cli/explain.go, internal/core/surface/sentences.go, internal/core/surface/examples.go, internal/surface/cli/helpgroups.go, commands/explain.md, .abcd/development/brief/04-surfaces/41-explain.md, .abcd/development/brief/04-surfaces/README.md, .abcd/development/release/surface.json, docs/reference/cli/commands.md
- tests: internal/surface/cli/explain_test.go
- The per-command JSON, the page blocks and the first instruction
- needs: 1
- packages: cmd/command-facts-sync, Makefile, commands, docs/reference/cli/commands
- tests: internal/surface/cli/commandfacts_drift_test.go, cmd/command-facts-sync/main_test.go
- The prose lint, with the pages made to pass it
- needs: 3
- packages: internal/core/lint/commandpagefacts.go, commands
- tests: internal/core/lint/commandpagefacts_test.go, internal/surface/cli/commandpage_lint_test.go
Frontmatter
| id | spc-2610301431075964 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610301431075964-an-agent-working-in-an-abcd-managed.md
open on GitHub ↗ · commit history ↗
References1
- itd-2610301346085246An agent learns how to call abcd from the abcd it runsimplements
mentions7
- iss-2610301320457709abcd has no machine-readable list of its verbs: abcd --help --agent --json ignores --json and prints the text menu, so an agent learns the verbs, their flags and how to call them only by parsing help prose or reading every commands/*.md page. The list must be built from the same source as --help --agent and must carry machine-readable instructions for an agent to use abcd.
- itd-111A Stale abcd Never Answers Silently
- itd-2610301327282266abcd's help fits the screen it is read on
- itd-2610301428378830abcd tells an agent what to do next, after a refusal and after a success
- itd-2610301428390406An agent can practise a filled-in form before it counts
- spc-2610301431108175abcd tells an agent what to do next, after a refusal and after a success
- spc-2610301431131636An agent can practise a filled-in form before it counts