core.md doubled as the folder index: its intro claimed "this folder catalogs the data structures" and carried the 38-row page table, wording that predates the one-page-per-subsystem shape where every page also carries its generated Cordis surface. The folder index now lives in docs/subsystems/README.md (page table plus the type-equiv note), and core.md is one subsystem page among siblings: the spine vocabulary. Structural referents move with it: the docs/AGENTS.md tier table and update rule, development.md's type-equiv pointer, the dsh-code-review skill, the two owning catalog Agent Notes, and website/docs.ts (README projects as reference/subsystems/index.md and takes the docs/subsystems folder alias; sidebar orders shift by one). Remaining "data-structure catalog" / "sub-page" phrasing in active notes and READMEs is reworded to subsystem-page terms in both languages; touched pairs re-recorded; translation-prompt snapshot re-recorded (its example embeds development.md).
5.9 KiB
Agent Note: Generated tool-schema catalog (boot-and-harvest)
Status: implemented
English | 中文
Problem
The repository had no single reference for the names, descriptions, and JSON Schemas actually exposed to the model. Source declarations are scattered and runtime-composed, while the existing Cordis reference and subsystem pages cover wiring and vocabulary rather than tools.
Decision
Generate the catalog by booting each tool plugin and reading its registered schemas, not by parsing source. scripts/gen-tool-catalog.ts mounts each shipped tool package on a fresh cordis Context (with SystemPrompt + ToolRegistry and the injected seams the plugin's apply reads), calls ctx.tools.schemas() — exactly the ToolSchema[] the model is sent — disposes the context, and renders one ## <package> section per package with a ```json parameters block per tool. It mirrors the gen-cordis-catalog / gen-module-graph CLI shape: default --write regenerates, --check fails if the committed copy is stale, output is deterministic (manifest-ordered, tools sorted by name). verify-tool-catalog (the --check) runs inside doc-sync, so relevant documentation changes and CI exercise the same freshness check.
Why boot, not parse (the crux)
The cordis catalog is a pure TypeScript-AST pass because every event/service name is a string literal that round-trips to a static declaration — the AST is the whole truth. Tool schemas are not statically knowable, so the same technique would produce a doc that lies:
tool-todowritesenum: [...STATUSES]— a spread of a runtimeconst. The AST sees the spread expression, not["pending","in_progress","completed"].- Every description is built by string concatenation (
'…' + '…'). The AST sees concatenation nodes, not the final prose the model reads. tool-subagent's tool name isconfig.toolName ?? 'subagent'— chosen at load, not a literal.- An MCP plugin can register raw JSON Schema directly via
ctx.tools.register()withoutdefineToolat all, so enumeratingdefineTool(call sites structurally under-counts.
The only faithful source of truth is the schema the registry actually holds after the plugin loads. Booting is the testing-policy discipline "verify the world, not the self-report" applied to a doc generator: read the shipped artifact, not a re-derivation of it.
Restoring "nothing silently omitted"
Booting has a cost the AST pass did not: there is no source declaration set to enumerate, so a new tool package could simply be forgotten. A completeness guard restores the guarantee — assertManifestComplete globs every tool-* package under packages/ and hard-errors if any is absent from the generator's boot manifest. A new tool package fails the generator, and therefore doc-sync, until it is registered. This is the same structural property the cordis generator gets for free from enumerating source, re-created for a boot-based generator.
A hand-maintained boot manifest is the irreducible policy
The filesystem discovers the tool-package inventory and the completeness guard rejects omissions. TOOL_PACKAGES still owns an explicit boot recipe for each package because required seam implementations and config are policy, not facts that can be inferred safely from layout or injection names.
Scope
Shipped product tool packages under packages/*/tool-*, each booted with its default config, including dsh-tool-bash (bash), dsh-tool-tasks (task_output, task_list, task_kill), and dsh-tool-subagent (subagent). Example-only tools are excluded.
The catalog unit is a package, not every configured tool instance. Each package boots once with default config; load-time aliases such as subagent_fork are noted without enumerating every deployment permutation. A deployment inventory is a separate, unbounded surface.
A plain json fence
Schema blocks use ```json, not a bespoke ts-family fence. doc-typecheck only extracts ts* fences, so a JSON block is invisible to it — no BlockKind wiring is needed (unlike the cordis catalog's ts cordis-catalog fence, which had to be allowlisted so a bare signature fragment isn't compiled).
Alternatives considered
- A pure TypeScript-AST pass, like the cordis catalog — tool schemas are not statically knowable (the crux above): runtime spreads, string concatenation, config-chosen names, and raw
ctx.tools.register()registrations all make an AST-derived doc lie. - Inferring each package's boot recipe from its injects — the "too clever" path the discover-package-inventory proposal warns against; the recipe stays hand-written policy while the inventory is discovered and completeness-guarded.
- A bespoke
ts-family fence for schema blocks — unnecessary: a plain```jsonfence is invisible todoc-typecheck, so noBlockKindallowlisting is needed.
Consequences
- The catalog cannot drift: a tool schema change the committed file doesn't reflect fails
verify-tool-catalogindoc-syncand CI. A newtool-*package not added to the manifest fails the completeness guard outright. - Tool description prose has a single home — the
defineTooldescriptionat the source — and the generated entry is only as good as it, the same forcing function the cordis catalog applies to event JSDoc. - The generator imports and executes workspace packages (the first repo script to do so; the others only read text). It runs under
tsxvia the roottsconfigpathsmap, the same unbuilt-source path the demos and tests use, so it needs no build step. - A new capability seam behind a future tool means a new manifest recipe entry (which seams to mount). This is the deliberate hand-written cost called out above; it changes only when a tool package is added.