Development record
A spec lists its steps, and the build lands them one at a time
Press Release
A spec lists its steps, and
abcd buildlands them one at a time, each its own lane and pull request."The implement verb was three lanes in the spec's order, and nothing had a word for the three," said a technical facilitator reading the run file. "Now the spec says its steps, the loop builds them in that order, and a step that does not fit the cut leaves the rest as the remainder. No new record family, no task tracker: a list in the design record."
Why This Matters
The record's only unit below a spec was another spec, minted as a remainder, and the build machinery's scope condition says a lane too large for one context is split by the spec, not by the loop. The run file already split the implement verb into three lanes with no name for the pieces. The field's answers are Shape Up's scopes (pieces inside the pitch) and Kubernetes' in-record graduation; a child record family (Linear's sub-issues) adds folders and verbs the vocabulary is shedding. Ruled on 2026-09-21 (adr-2609212115255771, decision 4): a section in the spec, called steps, sharing the loop's own word.
Mechanism
We expect specs split into named steps to land with fewer fix rounds and smaller lane contexts than the pilot's 349k-token lane, because a step is what one implementer can hold and land; shown wrong if stepped specs show no smaller lane contexts than unstepped ones of the same footprint.
Scope Conditions
None stated.
What's In Scope
- The section:
## Stepsin the spec, an ordered list, each step with a title and its footprint (packages, tests); the spec template seeds it empty atintent plan; a spec without steps is one step. - The loop:
abcd buildreads the list and runs one lane per step in order, each landing as its own pull request; the next step's lane starts after the previous has merged. - The remainder: a step that does not fit the cut leaves the unfinished steps in the remainder spec
spec close --remaindermints. - The brief: a lane's brief names the step it builds and the steps before it; the run record lists steps as it lists lanes.
- One word:
implement step(the loop's step interface) and this section share the word, and the command page says so.
What's Out of Scope
- The build proposing a split (the author writes the steps).
- A task record family.
- Reordering steps mid-run.
Decisions
Ruled by the product thinker on 2026-09-21, in the interview that filed and planned this intent (adr-2609212115255771 records the vocabulary rulings it rests on):
- The unit below a spec is a section called steps, not a record family (adr-2609212115255771, decision 4).
- The spec's author writes the steps; a spec without any is one step.
- A step that does not fit the cut leaves the rest as the remainder.
Open Questions
None open.
Acceptance Criteria
- Given
abcd intent planmints a spec, when the stub is read, then it carries an empty## Stepssection beside## Footprint, and a spec with no steps listed is built as one step. - Given a spec with three steps, when
abcd buildruns it, then three lanes run in order, each landing as its own pull request, and the second starts only after the first has merged. - Given a step that does not fit the cut, when the spec is closed with
--remainder, then the remainder spec carries the unfinished steps. - Given a lane for a step, when its brief is rendered, then it names the step and the steps before it, and the run record lists the steps.
- Given the command page, when it is read, then
implement stepand the spec's steps are described as one word for one thing.
Audit Notes
Fidelity review — receipt rcp-2257cc3a38d9 (verifier intent-auditor claude-fable-5-1).
Provenance: intent-auditor@claude-fable-5-1 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:9917c69a7540149729e8eb4af6ad1265e655f9d85331a4d795333c961f001612 Input attestations: diff:e5dfe0fc4 3f20b5167 3ebb7b0e3 8ede4810f 3c19408cb 3130ebca8 4651b460b (ancestors of 4651b460b, docs/close-spec-steps)@sha256:1041e178e12eb68b3ea1faca87bc624fd726a8b8643aff919dd04eb09321131a;
Acceptance rollup: MET 2 · MET_WITH_CONCERNS 3 · NOT_MET 0 · INCONCLUSIVE 0
Per-criterion verdicts:
- ac-1 — MET: stubSections renders ## Footprint then ## Steps with the italic placeholder; spec.Steps turns a section listing none into one implicit step, startStepsRow hands the loop one step for such a spec, and TestPlanMintsASpecWithAnEmptyStepsSection proves both at the plan verb (passes on an archive copy of HEAD) evidence: internal/core/spec/spec.go:308 — "{heading: "## Summary", body: summary}, {heading: FootprintHeading, body: footprintStubBody}, {heading: StepsHeading, body: stepsBody}" evidence: internal/core/spec/spec.go:289 — "const stepsPlaceholder = "_No steps listed: the spec is built as one step." evidence: internal/core/spec/steps.go:294 — "if len(steps) == 0 { return []Step{{Number: 1, Title: ImplicitStepTitle, Implicit: true}}, nil }" evidence: internal/core/intent/startcheck.go:320 — "row.Detail = sp.ID + " lists no steps: it is built as one step"" evidence: internal/core/intent/steps_test.go:24 — "func TestPlanMintsASpecWithAnEmptyStepsSection" evidence: internal/core/spec/steps_test.go:67 — "func TestStepsOfAnUnsteppedSpecIsOneImplicitStep"
- ac-2 — MET_WITH_CONCERNS: readyPending opens a step's lane only when landedInRun holds for every step requires() names (all earlier steps without a needs line, DR6b), landedInRun demands Stage == StageDone, and a lane reaches done only after landMerged fetches origin's default branch and proves the pushed head an ancestor of it (an armed-but-unmerged pull request is a contention, not done); each lane opens its own pull request (lane.PR). Concerns: the merge-gate test runs two steps, not three, and the three-step in-order test (TestTheRecordNamesEachLaneAsItOpens) uses fake stages with no landing or pull request, so the criterion's three-lanes-three-pull-requests scenario is composed from parts, never exercised end to end; and because the gate is an ancestor check, a squash- or rebase-merged pull request is refused rather than counted merged, so under those merge methods the second lane never opens evidence: internal/core/implement/loop/schedule.go:182 — "func (s State) readyPending() []int" evidence: internal/core/implement/loop/schedule.go:172 — "if l.SpecStep == n && l.Stage != StageDone { return false }" evidence: internal/core/implement/loop/schedule.go:151 — "func (p PendingStep) requires() []int" evidence: internal/core/implement/loop/land.go:989 — "netGit(c.RepoRoot, "fetch", "--quiet", "--no-tags", Remote, "+refs/heads/"+def+":"+tracking)" evidence: internal/core/implement/loop/land.go:996 — "on, err := gitutil.IsAncestor(c.RepoRoot, ld.Pushed, tracking)" evidence: internal/core/implement/loop/land.go:1015 — "return Outcome{}, contend(string(StageLand), "", lane.ID, "pull request #"+n+" is not merged yet" evidence: internal/core/implement/loop/land.go:1008 — "pull request #"+n+" merged, but the lane's head "+shortSHA(ld.Pushed)+" is not on "+Remote+"/"+def+" (a squash or rebase rewrote it)" evidence: internal/core/implement/loop/parallel_test.go:498 — "f := newParFixture(t, "1. One\n2. Two\n", Options{SubAgents: strp("3")})" evidence: internal/core/implement/loop/parallel_test.go:518 — "no lane opens for step 2 while lane 1's pull request is not merged, with every slot free" evidence: internal/core/implement/loop/parallel_test.go:273 — "f.repo.Git("push", "-q", "origin", l.Landing.Pushed+":refs/heads/main")" evidence: internal/core/implement/loop/loop_test.go:861 — "specWithSteps("1. The parser\n2. The loop\n3. The page\n")" evidence: internal/core/implement/loop/loop_test.go:866 — "f := &fakeSteps{calls: map[Stage]int{}}" evidence: internal/core/implement/loop/land_test.go:321 — "if r := mustRefusal(t, err); !r.Contention || !strings.Contains(r.Reason, "not on")" evidence: internal/core/implement/loop/parallel_test.go:613 — "strings.Count(f.ghLog(t), "pr merge 7 --auto") != 1"
- ac-3 — MET_WITH_CONCERNS: the --remainder close reads the closing spec's steps, carries the ones without a
- landed:line into the minted remainder renumbered from one (CarryUnlanded, CreateWithSteps), and refuses writing nothing when the section cannot be read; TestReconcileRemainderCarriesTheUnlandedSteps passes on the archive copy. Concern: 'unfinished' is decided solely by a hand-written- landed:marker the delivery chose outside the record (e5dfe0fc4's message says so), and nothing in the loop's landing writes that line, so a step the loop landed as a pull request is carried into the remainder unless the author marks it by hand evidence: internal/core/intent/lifecycle.go:932 — "listed, err := spec.ReadSteps(repoRoot, sp)" evidence: internal/core/intent/lifecycle.go:939 — "carried, rewrites = spec.CarryUnlanded(listed)" evidence: internal/core/intent/lifecycle.go:987 — "minted, err = spec.CreateWithSteps(repoRoot, intentID, remainder.Slug, remainder.ProductionMode, carried)" evidence: internal/core/spec/steps.go:316 — "func Unlanded(steps []Step) []Step" evidence: internal/core/spec/steps.go:248 — "func CarryUnlanded(steps []Step) ([]Step, []NeedsRewrite)" evidence: internal/core/intent/steps_test.go:46 — "func TestReconcileRemainderCarriesTheUnlandedSteps" evidence: internal/core/implement/loop/land.go:1043 — "pull request #%s landed: %s is on %s/%s at %s; removed the lane's worktree and branch %s" - ac-4 — MET: renderEarlierSteps writes the lane's step among the spec's and each earlier step with what landed it (the base's
landed:line and the run's lane with branch, head and pull request), a first step or an unstepped spec saying no step comes before; the run record carries every lane's spec_step and step_title, the pending steps, and anopenentry naming each lane's step as it opens, rendered as 'spec step N' in text; TestTheBriefNamesTheStepsBeforeItsOwn and TestTheRecordNamesEachLaneAsItOpens pass on the archive copy evidence: internal/core/implement/loop/brief.go:589 — "fmt.Fprintf(b, "This lane builds step %d of the %d %s lists", lane.SpecStep, len(steps), st.Spec)" evidence: internal/core/implement/loop/brief.go:595 — "for _, s := range steps[:lane.SpecStep-1]" evidence: internal/core/implement/loop/brief_steps_test.go:75 — "1. "The parser" — landed: #12 (the spec at the lane's base)" evidence: internal/core/implement/loop/record.go:114 — "rl := RecordLane{ID: l.ID, Key: l.Key, SpecStep: l.SpecStep, StepTitle: l.StepTitle, Stage: l.Stage" evidence: internal/core/implement/loop/record.go:44 — "Pending []PendingStepjson:"pending"" evidence: internal/core/implement/loop/schedule.go:252 — "st.Record = append(st.Record, Entry{At: now, Lane: l.ID, Stage: "open", Note: fmt.Sprintf("%s opened for step %d of %s (%s)"" evidence: internal/core/implement/loop/loop_test.go:905 — "lane-1: checks passed; lane-1 opened for step 1 of spc-1 (The parser)" evidence: internal/surface/cli/build.go:1003 — "fmt.Fprintf(w, " %s: spec step %d, %q — %s\n", l.ID, l.SpecStep, termsafe.Sanitize(l.StepTitle), l.Stage)" - ac-5 — MET_WITH_CONCERNS: commands/intent.md and commands/build.md now say 'step' names the spec's piece and nothing else, a lane's are its stages, and the payloads keep them apart as spec_step and stage; the glossary says the same. Concern: this is the signed-off divergence BU1 (iss-2609291313276243), not the intent's 'share the word' plan — the verb
implement stepkeeps a name that the same page says denotes one stage or one move of the run, not one step, so the verb's own name remains a second use of the word that the page explains rather than removes evidence: commands/intent.md:592 — "In the build loop, "step" names this and nothing else: the loop (/abcd:build) lands each step as one lane and takes the lane through its stages" evidence: commands/intent.md:594 — "abcd implement stepperforms one stage, and its payloads name the lane's stage understageand the spec's step underspec_step" evidence: commands/build.md:16 — "Two words, two things. A step is a piece of the spec" evidence: commands/build.md:19 — "implement stepperforms one move of the run" evidence: .abcd/development/brief/glossary/core/step.md:18 — "abcd implement stepperforms one stage, not one step" evidence: .abcd/development/intents/shipped/itd-2609212103565953-a-spec-lists-its-steps-and-the-build.md:48 — "implement step(the loop's step interface) and this section share the word, and the command page says so"
Gap audit:
- honoured:
- the spec template seeds an empty ## Steps section at intent plan and an unstepped spec is one step evidence: internal/core/spec/spec.go:308 — "{heading: StepsHeading, body: stepsBody}" evidence: internal/core/spec/steps.go:294 — "Implicit: true"
- a step's lane opens only after every earlier step's pull request is an ancestor of origin's default branch, each step landing as its own lane and pull request evidence: internal/core/implement/loop/schedule.go:172 — "l.Stage != StageDone" evidence: internal/core/implement/loop/land.go:996 — "gitutil.IsAncestor(c.RepoRoot, ld.Pushed, tracking)"
- spec close --remainder carries the steps not marked landed into the remainder evidence: internal/core/intent/lifecycle.go:939 — "spec.CarryUnlanded(listed)"
- the brief names the lane's step and the steps before it, and the run record lists the steps evidence: internal/core/implement/loop/brief.go:595 — "steps[:lane.SpecStep-1]" evidence: internal/core/implement/loop/schedule.go:252 — "Stage: "open""
- no task record family and no build-proposed split: steps are a section of the spec the author writes evidence: internal/core/spec/steps.go:20 — "const StepsHeading = "## Steps"" evidence: .abcd/development/brief/glossary/core/step.md:22 — "a step is a section of the design record, not a record of its own"
- diverged:
implement stepand the spec's steps share one word for one thing: delivered as a rename (BU1) under which 'step' is the spec's piece only and the verbimplement stepperforms a stage evidence: commands/build.md:16 — "Two words, two things." evidence: commands/intent.md:594 — "abcd implement stepperforms one stage"- 'the next step's lane starts after the previous has merged' proven for three steps: the real-landing test runs two steps, and the three-step test runs fake stages with no pull request evidence: internal/core/implement/loop/parallel_test.go:498 — ""1. One\n2. Two\n"" evidence: internal/core/implement/loop/loop_test.go:866 — "fakeSteps"
- 'a step that does not fit the cut leaves the unfinished steps in the remainder' rests on a hand-written
- landed:marker the loop never writes evidence: internal/core/spec/steps.go:319 — "if s.Landed == "" && !s.Implicit" evidence: internal/core/implement/loop/land.go:1042 — "ld.Merged = merged"
- missing: (none)
Grounds
- pursued: the run file already builds the implement verb as three lanes in the spec's order with no word for the pieces; we expect named steps to land with smaller contexts and fewer fix rounds; shown wrong if stepped specs show no smaller lane contexts
Frontmatter
| id | itd-2609212103565953 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | standalone |
| severity | minor |
.abcd/development/intents/shipped/itd-2609212103565953-a-spec-lists-its-steps-and-the-build.md
open on GitHub ↗ · commit history ↗
References3
- itd-2609201916151817One verb takes a READY intent to delivered, and refuses while a decision is openbuilds on
- itd-2609211116005482`abcd build next` picks the readiest planned intent itself, and writes down whybuilds on
- itd-2609211913453478One page maps the record families, and phase, milestone and roadmap are retiredrelated
Referenced by12
- adr-2609212115255771Phases and milestones are retired: sequencing is dependencies rendered as status, the checkpoint is the derived releaserelated
- adr-2609292012006845Now, Next and Later list an intent in a lane under Now only; phases stay retiredrelated
- iss-2610262032001815The mental model says per-step lanes are a planned intent; that intent is on the shipped shelf and says the build lands steps one lane at a time.related
- iss-2610262037500468The spec-steps intent has `implement step` and the spec's steps share one word; the build chapter says steps and stages are two words for two things.related
- iss-2610262037509765The spec-steps intent starts each step's lane only after the previous step merged; the build chapter opens a lane once the steps its needs line names have landed, so lanes can run side by side.related
- iss-2610262041398357The record-families page lists the step intent itd-2609212103565953 as planned; that intent is filed under shipped/.related
- iss-2610262041402639The step glossary entry calls itd-2609212103565953 planned, but that intent has shipped.related
- iss-2610262041419578The step intent says `implement step` and the spec's Steps section share one word; the step glossary entry says `implement step` performs a stage, not a step.related
- iss-2610262045422454itd-2609212103565953 starts each step's lane only after the previous has merged; itd-2609201925079472 runs independent steps' lanes side by siderelated
- iss-2610262045557535itd-2609211913453478's record-families page has a row for step; itd-2609212103565953 says a step is a section, not a record familyrelated
- itd-2609211913453478One page maps the record families, and phase, milestone and roadmap are retiredbuilt on by
- spc-2609212138246060a-spec-lists-its-steps-and-the-build-lands-them-one-at-aimplemented by
mentions10
- dcn-26092100000000012026-09-21 — Phases and milestones are retired, and the word roadmap with them (product thinker, at an interactive interview after an independent research pass; adr-2609212115255771 supersedes adr-9). Sequencing is dependencies plus the lifecycle shelves, rendered as a Now / Next / Later status block on the `abcd` board and the site (itd-2609212103568351), never stored; the checkpoint is the derived release plus each intent's acceptance criteria, with an optional `target_release` the cut reports and moves forward (itd-2609212103572513); the unit below a spec is the step, a section not a family (itd-2609212103565953); an issue carries no spec by design; the batch is the run's internal order; one page maps the families (itd-2609211913453478). itd-24 becomes release retrospectives; itd-34 drops its phase rule. Same interview: `build` is the verb a person types and `implement` the loop (itd-2609201916151817 decision 8), the loop takes an issue key (decision 10), only the loop writes a verdict (decision 9); the person's command list is grouped with an agents block behind `--agent` (itd-146), every verb opens with a does/writes/refuses sentence (itd-2609212113220149), modes become flags and five checks one lint (itd-2609212130136102, breaking); the status-line badge is guarded and reset (itd-2609212130146198); a new capture or draft is matched and linked at filing, never refused (itd-2609212137116617); abcd's text names the product thinker or the technical facilitator and never the maintainer (itd-2609212137129937, captured for the run, no sweep today); the lab conventions become `abcd lab` (itd-2609212137128014); a managed repository's site goes live by one verb behind a provider seam (itd-2609061543533170).
- dcn-26093000000000032026-09-30 — BU1 and BT1 are applied under H6, the person's approval of v0.12.0 as a breaking release in the human-step interview ("next release: APPROVED as BREAKING v0.12.0 (lane-stage rename per BU1 + old command spellings removed per BT1), with a migration note in the release notes", 2026-09-29), by autonomous run A's lane breakingStages. The implement loop's lane stages are `stage`: the run state file's lanes and record lines, the `implement step` and `implement receipt` results (`performed` became `performed_stage`, `step` became `stage`), every loop refusal (`refusal.stage`) and the status board's lane; the spec's `spec_step`, `step_title` and `pending` keep "step", and the verb keeps its name, `implement step` performing one stage. The state file is schema version 4, and a file of versions 1 to 3 is migrated on read, never rewritten by the read: each `step` is carried over to `stage` in memory and the run's next mutation writes version 4, as the loop already did for versions 1 and 2, while such a file that already says `stage` is refused. Migration was chosen over refusing the old file so a run in progress survives the upgrade (the lane's technical ruling). The one-release stubs itd-2609212130136102 left are removed in the same cut: `ahoy dry-run`, `ahoy identity-check`, `version` (with `--check`), `docs lint` and `site check` are unknown commands, and bare `identity` and bare `ahoy remote` list their sub-verbs; the CLI's moved-spelling machinery goes with them while the release snapshot keeps its `moved_to` field, which the snapshots the guardrail diffs against still carry. The upgrade guide `docs/how-to/upgrade-to-v0.12.0.md` names every renamed field and removed spelling with its replacement. BU1 ruled on the build loop only, so the shared run's `implement check` vocabulary, which also says "step", is left as it is and captured as iss-2609292359485570 for a ruling. Resolves iss-2609291313276243 and iss-2609251324599468; criterion 5 of itd-2609212103565953 is met, and its spec stays open for criterion 2.
- dcn-26093000000000052026-09-30 — Ruling DR6 (the product thinker, 2026-09-29, verbatim: "per-run agent limit: WORK IN PARALLEL — a run may build several pieces and run reviewers concurrently up to its limit (new build-loop work; then AC6 is testable).") is designed into spc-2609202134341288 as its piece 6 and recorded as decision 7 of itd-2609201925079472 (lane dr6Spec of autonomous run A; records only, nothing built). A slot is one outstanding await in the run's state file, implementers and validators alike; the validators of a round run side by side; a spec step runs beside earlier ones only when it declares `- needs:` (absent, it needs every earlier step, so itd-2609212103565953's in-order landing holds for every spec written so far); a lane lands one at a time and, when a sibling landed after its base, is merged with the default branch (never rebased) and judged by a fresh round, a sync that does not count against `--fix-rounds`. Criterion 6 is tested through the spec's criteria C1 to C12.
- dcn-26093000000000202026-09-30 — Rulings DR6b and DR6c (the product thinker, 2026-09-30, relayed by the orchestrator of autonomous run A) close the two questions the DR6 spec review left open in spc-2609202134341288 and are recorded as decision 8 of itd-2609201925079472 (lane dr6Spec, amend round 3; records only, nothing built). DR6b, verbatim: "(a) ONE AFTER ANOTHER BY DEFAULT: a step runs alongside earlier ones only if its plan says so; nothing already planned changes; reviews run side by side; the 2026-09-21 wording stands." A step without a `- needs:` line needs every earlier step, so itd-2609212103565953's wording of 2026-09-21 stands. DR6c, verbatim: "(c) FINISH, BUT HOLD THEM: when one piece is handed back, pieces in flight finish but nothing merges until the person re-plans; the person then decides whether the held pieces land as they are." After a hand-back the sibling lanes run to completion and each passing one takes the lane stage `held` (before its push, or before arming once pushed; an armed one is disarmed), recorded in `lanes[].hold` and shown by `implement status`; the person releases or discards each with `implement step --release <lane>` or `--discard <lane>` (the claim verb `implement release` is a different noun and is not reused). Tested by the spec's criterion C13.
- iss-2609251617091944The record-families page's Moved by column names verbs the binary does not have: intent reclassify, abcd build, intent plan --bundle and drain are not in go run ./cmd/abcd --help, and the bundle glossary entry claims abcd intent plan itd-A itd-B --bundle <name> as present; itd-34 (multi-intent plan) and itd-2609212103565953 (steps and the build) are both planned. The page every glossary entry points at teaches a reader commands that refuse.
- iss-2609291313276243The glossary entry for step (.abcd/development/brief/glossary/core/step.md) says the loop's step interface, abcd implement step, uses the same word for the same thing as a spec's steps: the next piece of the current spec. The loop does not work that way: implement step performs one stage of the current lane (worktree, brief, implement, validate, land), and a lane lands one spec step, so the state file, the JSON payloads (step, performed, refusal.step) and the command pages already use one word for two things and qualify the spec's as spec step. itd-2609212103565953 criterion 5 asks the command page to describe them as one word for one thing, which cannot be written truthfully until someone rules which of the two keeps the word (rename the lane's stages, or change what implement step means) or restates the criterion.
- iss-2610110918418459The implement loop's landing never writes a spec step's '- landed:' line, so a spec the loop landed part of still reads those steps as unlanded: a later 'spec close --remainder' carries steps that already merged into the remainder, and a fresh 'abcd build' of the same spec opens a lane for them again. Found by the fidelity audit of itd-2609212103565953 (receipt rcp-2257cc3a38d9, criterion ac-3 MET_WITH_CONCERNS); spc-2609202134338445's Progress already says the landing does not mark a non-closing lane's step.
- iss-261025221517445803-mental-model.md labels itd-2609212103568351, itd-2609212103565953 and itd-34 as 'planned', but all three are in intents/shipped/, and abcd intent plan <itd-N> [<itd-N>…] --bundle ships.
- iss-2610262030591199The mental model calls per-step lanes planned (itd-2609212103565953), while the build chapter describes one lane per spec step as shipped behaviour
- spc-2609202134341288an-autonomous-implementation-run-paces-itself-by-default-and