specification · diff
git:20260908.d2bb58e to git:20260908.91ee09b
31 added, 35 removed. Audit A to A.
---
name: specification
- description: Use when transcribing an intent document into the requirements ledger, deriving the partition, design docs and constraints from it, or updating them from rework feedback; not for RTL implementation or verification.
+ description: Use when transcribing an intent document into the requirements ledger, deriving the design boundary, architecture and timing constraints from it, or updating them from rework feedback; not for RTL implementation or verification.
---
# Requirements and Specification Freeze
Your sole responsibility: turn the delivered `<intent>/brainstorm.md` into a frozen design
- source of truth. You dispatch and hold no document body: the intent document, the ledger,
- `design.md` and every `<child>.md` are read and written only inside sub-Task contexts.
+ source of truth. You dispatch and hold no document body: the intent document, the ledger and
+ `design.md` are read and written only inside sub-Task contexts.
+ What this stage freezes is the **boundary** — the rows, the clocks, the ports, the obligations the
+ design is held to at that boundary, and the timing scenarios. **Which RTL modules exist is not
+ yours to fix.** `design.md` argues for a structure, and the engineer's rows constrain it, but the
+ module split is decided by whoever writes the RTL against the boundary you froze.
+
## Iron Rule
Write only under `{workdir}`; never another module's artifacts. Reading templates and upstream inputs outside it is fine.
## Artifacts
`<skill>` is this skill's own base directory, named on the first line of this file.
- Read `{workdir}/dispatch.json` for this round's inputs: its `inputs` table maps each upstream key to a location, so `<key>/<subpath>` is how you address one. The only input is the intent tree, the container `<intent>/`: `brainstorm.md`, in whatever shape the engineer wrote it, plus whatever the engineer delivered with it and the document names as authoritative. Frozen for the run. The document is read whole only by the transcriber and the requirements reviewer; another file in the container is read by whoever a row pointing at it concerns.
+ Read `{workdir}/dispatch.json` for this round's inputs: its `inputs` table maps each upstream key to a location, so `<key>/<subpath>` is how you address one. The only input is the intent tree, the container `<intent>/`: `brainstorm.md`, in whatever shape the engineer wrote it, plus whatever the engineer delivered with it and the document names as authoritative. Frozen for the run. The document is read whole only by the transcriber and the reviewers; another file in the container is read by whoever a row pointing at it concerns.
Everything below is produced under `{workdir}`. Each JSON sidecar's shape is `references/<name>.schema.json`.
| Path | What it is |
|---|---|
| `requirements.json` | The ledger: one row per proposition the intent document states, in the engineer's words, with `judge` naming who establishes it. Every downstream rule binds it |
- | `manifest.json` | The child partition: `module` + `children[]` (`name` / `doc` / `rtl_modules[]`) |
- | `design.md` | The decisions made around the ledger: architecture, partition rationale, inter-module behaviour contract, timing scenarios and waveforms (template: `references/design-template.md`) |
- | `children/<child>.md × N` | Per-child sub-design (template: `references/child-design-template.md`); `children/` leaves as one tree |
- | `clocks.json`, `top-io.json`, `interconnects.json` | The boundary: clocks, top-level ports, cut edges |
+ | `manifest.json` | `module` — the top RTL module's name, and the only thing here. Six readers resolve `<TOP>` from it |
+ | `design.md` | The decisions made around the ledger: the architecture it proposes, the boundary narrative, the obligations more than one part must jointly keep, and the timing scenarios (template: `references/design-template.md`) |
+ | `clocks.json`, `top-io.json` | The boundary: clocks and their arrival budgets, top-level ports |
| `check-hints.json` | How simulation observes each ledger row it judges |
| `constraints/<TOP>.sdc`, `constraints/<TOP>.sgdc` | Generated by `derive-constraints` |
- | `spec-review/decisions.md` | The user's rulings at both gates, in their words. It accumulates across rounds — a ruling this round did not revisit still stands, so it is carried forward and appended to, never rewritten |
- | `spec-review/findings/` | `requirements.md` (the ledger against the intent document) and one review per child. This round's record: it does not carry into the next round, so a review on disk is always about what is on disk now |
+ | `spec-review/decisions.md` | The user's rulings, in their words. It accumulates across rounds — a ruling this round did not revisit still stands, so it is carried forward and appended to, never rewritten |
+ | `spec-review/findings/` | The two reviews. This round's record: it does not carry into the next round, so a review on disk is always about what is on disk now |
| `result.json` | The status envelope |
## Fan-out
- **Dispatch-and-wait:** after dispatching, send a brief status and end the turn; a wave's gate begins once every sub-Task in it has reported.
- - **Sub-Task `STATUS: BLOCKED`:** a crash, not a `fail` verdict. Finalize with a `--fail-reason` listing the failed children — it lands in the envelope's `stage_specific.fail_reason` — and leave per-child re-dispatch to a repair round.
+ - **Sub-Task `STATUS: BLOCKED`:** a crash, not a `fail` verdict. Finalize with a `--fail-reason` naming what failed — it lands in the envelope's `stage_specific.fail_reason` — and leave re-dispatch to a repair round.
## Workflow
### Which round is this
Read `{workdir}/dispatch.json` first. The kernel writes `scope` / `caused_by` / `reasons` **only when they carry something**, so their presence is what tells you:
- - **`caused_by` present — failures downstream are waiting on this stage.** Each entry is one failing run's own `result.json`, and a round scheduled for some other reason carries them too: answer them in this round. This stage already shipped, and rtl-design / simulation-plan consumed the manifest, so the partition is not this round's to change. Scope is the union of `dispatch.json`'s `scope` and what the `caused_by` envelopes attribute; Read each envelope once. Dispatch one repair sub-Task over the decisions (`design.md`, the children, the sidecars), then pick up at **`check-crossrefs`** and run through to finalize: the authoring dispatches are skipped, but the join and the constraints must re-verify against what changed, and the child reviews re-run so the promoted review is never stale. The ledger is not re-transcribed: the intent document is frozen, and a row changes only by a human's ruling.
+ - **`caused_by` present — failures downstream are waiting on this stage.** Each entry is one failing run's own `result.json`, and a round scheduled for some other reason carries them too: answer them in this round. Scope is the union of `dispatch.json`'s `scope` and what the `caused_by` envelopes attribute; Read each envelope once. Dispatch one repair sub-Task over the boundary and `design.md`, then pick up at **`check-crossrefs`** and run through to finalize: the authoring dispatches are skipped, but the join and the constraints must re-verify against what changed, and the delivery review re-runs so the promoted review is never stale. The ledger is not re-transcribed: the intent document is frozen, and a row changes only by a human's ruling.
- **`caused_by` absent — a first delivery.** Run every step below.
Either way your previous round, if any, is already in `{workdir}`: edit it in place, touching only what this round requires. Rewriting an artifact this round did not change still changes its bytes, and every stage downstream declares these files as inputs, so a cosmetic rewrite invalidates their proofs and buys a rebuild of everything transitively below for no change in content. Row ids never change and are never renumbered; a row added in a repair takes a fresh id.
A `{workdir}` already holding part of a round means the session was compacted or interrupted: that work is yours to continue or redo, and artifacts on disk are not a gate you already passed.
### Transcribe
Dispatch one Level-1 sub-Task per `references/ledger-task-contract.md`. In its own context it reads `<intent>/brainstorm.md` and writes `requirements.json`.
**Gate, script.**
```bash
python3 <skill>/scripts/spec/__main__.py check-ledger --workdir {workdir}
```
A non-zero exit names every shape or content defect on stderr (an unknown judge, a duplicate id, an `unassignable` row with no reason); re-dispatch the transcriber. On success stdout is the gate view: the count per judge, and verbatim the rows judged `unassignable`, `outside` and `human`, and the rows carrying a `target`. Keep it; the human gate below hands it over.
### Decompose
- Dispatch one Level-1 sub-Task per `references/decompose-task-contract.md`. It reads `requirements.json`, not the intent document, and writes `manifest.json`, `design.md`, `clocks.json`, `top-io.json`, `interconnects.json`. The author holds the rows and their ids and nothing else, so the design cites requirements rather than restating them.
-
- **Gate, script.** Run `derive-ports` to compute each child's inter-module ports, the `interconnects.json` wires whose producers or consumers include one of that child's `rtl_modules`:
-
- ```bash
- python3 <skill>/scripts/spec/__main__.py derive-ports --workdir {workdir}
- ```
-
- It also decides the top-partition purity rule, since this is the last moment the partition is still editable. On success the port map is on stdout, and the child sub-designs inject it. A non-zero exit names the defect on stderr; every one of them routes a decompose rework sub-Task.
+ Dispatch one Level-1 sub-Task per `references/decompose-task-contract.md`. It reads `requirements.json`, not the intent document, and writes `manifest.json`, `design.md`, `clocks.json` and `top-io.json`. The author holds the rows and their ids and nothing else, so the design cites requirements rather than restating them.
- ### Requirements review
+ ### Review
- Dispatch one Level-1 reviewer per `references/requirements-review-task-contract.md`. It reads the intent document, the ledger, the three sidecars and `design.md`, and writes `spec-review/findings/requirements.md`: what the document states that no row carries, what a row states that the document does not, which sidecar entries no row supports, where `design.md` contradicts or restates a row.
+ Dispatch one Level-1 reviewer per `references/review-task-contract.md`, naming its output `requirements.md`. It reads the intent document and everything on disk so far, and reports where any two of them disagree.
- **Gate, human — the ledger and partition gate.** Path-handoff, echoing no body, with four exceptions the gate view already holds verbatim: the `unassignable` rows with their `note`, the `outside` rows, the `human` rows, and the rows carrying a `target`. Add the `requirements.md` path, the N-child summary from manifest metadata (`Grep manifest.children[].{name,rtl_modules}`) plus the `derive-ports` map, and the `design.md` path.
+ **Gate, human — the ledger and boundary gate.** Path-handoff, echoing no body, with four exceptions the gate view already holds verbatim: the `unassignable` rows with their `note`, the `outside` rows, the `human` rows, and the rows carrying a `target`. Add the `spec-review/findings/requirements.md` and `design.md` paths.
- What the user decides here: every `unassignable` row (how it is measured, who judges it, or that it is not a requirement, in which case it becomes `none` with the reason in `note`); each `human` row, which they judge now from the documents in front of them; the partition. Write each ruling to `spec-review/decisions.md` in **their words, not yours**, then dispatch a transcribe repair sub-Task that edits the rows it names — nothing outside a sub-Task edits an artifact. After any edit to the ledger, re-run `check-ledger` and the requirements review. A reported omission or a partition change re-enters at transcribe or decompose.
+ What the user decides here: every `unassignable` row (how it is measured, who judges it, or that it is not a requirement, in which case it becomes `none` with the reason in `note`); each `human` row, which they judge now from the documents in front of them; and the boundary, which the hints and the constraints derive from next, so this is the last cheap moment to change it. Write each ruling to `spec-review/decisions.md` in **their words, not yours**, then dispatch a repair sub-Task that edits what it names — nothing outside a sub-Task edits an artifact. After any edit to the ledger, re-run `check-ledger` and this review. A reported omission re-enters at transcribe, a boundary change at decompose.
- ### Child sub-designs (×N)
+ ### Check hints
- Dispatch one sub-Task per child, each writing `children/<child>.md` per `references/child-design-template.md`, and alongside them one sub-Task writing `check-hints.json` per `references/check-hints-task-contract.md`. Inject each child's wire list from `derive-ports`; the child adds any top-IO ports it drives or reads. The hints are authored here rather than earlier because a row the human just ruled on can have become one simulation judges.
+ Dispatch one Level-1 sub-Task per `references/check-hints-task-contract.md`, writing `check-hints.json`. It runs here rather than beside decompose because a row the human just ruled on can have become one simulation judges.
- **Gate, script.** Run `check-crossrefs`. N children authored their docs and check hints in parallel, so it reports what only a join can see: a name one of them wrote that resolves nowhere, a target nobody claimed, a hint naming a row simulation does not judge, a row simulation judges that no hint names.
+ **Gate, script.** Run `check-crossrefs`. The ledger, the boundary and the hints are authored by different sub-Tasks, so it reports what only a join can see: a hint naming a row that is not the ledger's to hint, a row simulation judges that no hint names, a `check_id` used twice, a port whose clock domain no clock declares.
```bash
python3 <skill>/scripts/spec/__main__.py check-crossrefs --workdir {workdir}
```
- The verdict is structured on stdout: exit 0 clean, exit 1 the join found something. A non-zero exit with no verdict on stdout is exit 2 — a program exception, BLOCKED with the reason on stderr, most often a child doc the manifest names and nobody has written yet. **Fix nothing yourself.** Each disagreement names both sides, and which of the two is wrong is a judgment: the child may have mistyped a port, or the boundary may be missing it. Decide that, then route the rework to whoever authored that file, decompose for a sidecar and the affected child for a `<child>.md` or its check hints.
+ The verdict is structured on stdout: exit 0 clean, exit 1 the join found something. A non-zero exit with no verdict on stdout is exit 2 — a program exception, BLOCKED with the reason on stderr. **Fix nothing yourself.** Each disagreement names both sides, and which of the two is wrong is a judgment: the hint may name the wrong row, or the row may name the wrong judge. Decide that, then route the rework to whoever authored that file.
On a clean gate, immediately derive the constraints:
```bash
python3 <skill>/scripts/spec/__main__.py derive-constraints --workdir {workdir}
```
- It generates `constraints/<TOP>.{sdc,sgdc}` from `clocks.json` + `top-io.json`. Running it here, before the human gate, surfaces defects no cross-file join can see (the exactly-one-`primary` rule, clock-name collisions) while a rework is still cheap. It reads only the boundary sidecars, so a defect it reports on stderr routes to decompose.
+ It generates `constraints/<TOP>.{sdc,sgdc}` from `clocks.json` + `top-io.json`. Running it here surfaces defects no cross-file join can see (the exactly-one-`primary` rule, clock-name collisions) while a rework is still cheap. It reads only the boundary sidecars, so a defect it reports on stderr routes to decompose.
- ### Child reviews (×N)
+ ### Delivery review
- Dispatch one Level-1 reviewer per `manifest.children[]` per `references/child-review-task-contract.md`, passing paths. Each writes its own `spec-review/findings/<child>.md`.
+ Dispatch one Level-1 reviewer per `references/review-task-contract.md`, naming its output `delivery.md`. Same contract as the first one, and everything the round delivers is now on disk — the ledger the human ruled on, the boundary, the hints, the constraints — so this is the pass where a hint's rule and a `specification`-judged row's realization can be checked at all.
- **Handoff, not a gate.** Give the user the paths, echoing no body: `design.md`, each `children/<child>.md`, the `check-crossrefs` verdict, one `spec-review/findings/<child>.md` per child. You do not summarize the findings, rank them, or decide which ones matter: a review relayed through your summary is your judgment wearing the reviewer's name.
+ **Handoff, not a gate.** Give the user the paths, echoing no body: `design.md`, the `check-crossrefs` verdict, and both `spec-review/findings/` paths. You do not summarize the findings, rank them, or decide which ones matter: a review relayed through your summary is your judgment wearing the reviewer's name.
- Nothing here asks the user for a verdict, and the round does not wait on one. What the reviews reach for is the engineering soundness no script can — port roles, reset polarity, clock relationships, whether the children realize the rows they cite — and endorsing that is `kernel.py pin`, which anchors to the review's content and is what `signoff` refuses without. A blocking finding nobody acts on therefore leaves the module unsignable rather than shipping under a word typed to keep the round moving.
+ Nothing here asks the user for a verdict, and the round does not wait on one. What the reviews reach for is the engineering soundness no script can — port roles, reset polarity, clock relationships, whether the boundary carries what the rows require — and endorsing that is `kernel.py pin`, which anchors to the review's content and is what `signoff` refuses without. A blocking finding nobody acts on therefore leaves the module unsignable rather than shipping under a word typed to keep the round moving.
- Act on what they say: a body change re-enters at the child sub-designs, a partition change at decompose, and a finding they tell you to accept as-is goes to `spec-review/decisions.md` in **their words, not yours**, so the reasoning travels with the review it overrode. A round that cannot deliver at all closes below with `--fail-reason`.
+ Act on what they say: a boundary change re-enters at decompose, a row change at transcribe, and a finding they tell you to accept as-is goes to `spec-review/decisions.md` in **their words, not yours**, so the reasoning travels with the review it overrode. A round that cannot deliver at all closes below with `--fail-reason`.
### Finalize
Every run ends here, an unresolvable failure included:
```bash
python3 <skill>/scripts/spec/__main__.py finalize --workdir {workdir} [--fail-reason "<one-line reason>"]
```
The status is derived, so you assert nothing about the outcome: a reason names what stopped the round, and its absence means the stage delivered what it owes. On the delivering path finalize re-validates `requirements.json` and refuses one that still carries an `unassignable` row, then re-runs `check-crossrefs` and `derive-constraints` in-process — both were clean at the cross-reference gate, so a failure now means an artifact was edited after that gate, which is BLOCKED rather than a routable fail. Exit 0 = `result.json` written. A non-zero exit is a program exception: BLOCKED, reason on stderr, never a `status=fail`.
## Return Contract
Control returns to the caller, which decides what runs next from `result.json`.
Your sole completion signal is `{workdir}/result.json` present with `status=pass`.