cyber-sdd 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.plugin/pins.json +3 -0
  2. package/LICENSE +21 -0
  3. package/agents/sdd-automaton.md +13 -2
  4. package/agents/sdd-scanner.md +85 -0
  5. package/agents/sdd-spec-judge.md +32 -2
  6. package/agents/sdd-warden.md +9 -0
  7. package/package.json +30 -23
  8. package/skills/align-spec/scripts/align-spec.mts +3 -2
  9. package/skills/blast-estimate/README.md +3 -5
  10. package/skills/blast-estimate/SKILL.md +2 -2
  11. package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
  12. package/skills/builder-impl-governance/SKILL.md +9 -1
  13. package/skills/builder-spec-governance/SKILL.md +13 -1
  14. package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
  15. package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
  16. package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
  17. package/skills/check-retired-terms/README.md +18 -0
  18. package/skills/check-retired-terms/SKILL.md +81 -0
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
  20. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
  21. package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
  22. package/skills/collision-ladder/README.md +3 -5
  23. package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
  24. package/skills/combat-log-governance/SKILL.md +43 -4
  25. package/skills/concept-index/scripts/concept-index.mts +3 -2
  26. package/skills/discover-plans/scripts/discover-plans.mts +5 -2
  27. package/skills/discover-specs/scripts/discover-specs.mts +5 -2
  28. package/skills/doctrine-loop/README.md +6 -0
  29. package/skills/doctrine-loop/SKILL.md +136 -2
  30. package/skills/formation-loop/SKILL.md +21 -1
  31. package/skills/gate-validation-governance/SKILL.md +2 -2
  32. package/skills/impl-producer-governance/SKILL.md +10 -1
  33. package/skills/init/scripts/wire-statusline.mts +5 -2
  34. package/skills/lifecycle-governance/SKILL.md +1 -1
  35. package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
  36. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
  37. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
  38. package/skills/mission-graph/README.md +3 -5
  39. package/skills/mission-graph/SKILL.md +72 -5
  40. package/skills/mission-graph/scripts/mission-graph.mts +505 -16
  41. package/skills/place-node/scripts/place-node.mts +3 -2
  42. package/skills/plan-retirement/README.md +5 -2
  43. package/skills/plan-retirement/SKILL.md +5 -1
  44. package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
  45. package/skills/plugin-contract-governance/SKILL.md +7 -1
  46. package/skills/remediation-governance/SKILL.md +36 -1
  47. package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
  48. package/skills/resolve-tracking/SKILL.md +2 -2
  49. package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
  50. package/skills/sdd/SKILL.md +1 -1
  51. package/skills/spec-format-governance/SKILL.md +5 -0
  52. package/skills/spec-gate/SKILL.md +18 -2
  53. package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
  54. package/skills/spec-gate/scripts/check-suite.mts +53 -17
  55. package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
  56. package/skills/spec-producer-governance/SKILL.md +2 -2
  57. package/skills/ssa-lowering/README.md +3 -5
  58. package/skills/start-mission/SKILL.md +8 -4
  59. package/skills/suite-format-governance/SKILL.md +43 -4
  60. package/skills/touch-set-correction/README.md +3 -5
  61. package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
  62. package/skills/verify-scenarios/SKILL.md +10 -3
  63. package/skills/verify-scenarios/scripts/verify-scenarios.mts +92 -8
@@ -39,8 +39,9 @@
39
39
  // Pure functions are exported for node:test; running the file directly drives the CLI.
40
40
  // No dependencies. Use --dry-run to print the planned deletions without touching the tree.
41
41
 
42
- import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs'
42
+ import { existsSync, readdirSync, readFileSync, realpathSync, unlinkSync } from 'node:fs'
43
43
  import { join } from 'node:path'
44
+ import { pathToFileURL } from 'node:url'
44
45
 
45
46
  const PLAN_SUFFIX = '.plan.md'
46
47
  const LOG_SUFFIX = '.log.jsonl'
@@ -193,4 +194,6 @@ export function main(argv: string[]): number {
193
194
  return 0
194
195
  }
195
196
 
196
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
197
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
198
+ process.exit(main(process.argv.slice(2)))
199
+ }
@@ -48,9 +48,15 @@ actor bars are the shipped `sdd:{oracle,builder,architect}-{spec,impl}-governanc
48
48
  self-aligns to exactly the bars its judge grades. The lens sets are spec gate `{oracle, builder,
49
49
  architect}`, impl gate `{builder, architect}`, solution `{architect}` (ungated).
50
50
 
51
+ **Read each row against that sentence.** A producer row that does not carry its whole lens set is a
52
+ transcription slip, not a narrowing — this table is a shipped copy of one owned by SDD's own spec
53
+ (`design/specialists-and-squads.md`), restated here because a governance loads standalone and cannot
54
+ reach the spec tree. It has drifted once: the spec-producer row lost `architect-spec`, and plugin
55
+ authors building to it shipped agents that loaded three bars and were graded against four.
56
+
51
57
  | Role | Loads |
52
58
  |---|---|
53
- | spec-producer | `spec-format`, `suite-format`, `ownership`, the resolved `oracle-spec` + `builder-spec` bars |
59
+ | spec-producer | `spec-format`, `suite-format`, `ownership`, the resolved `oracle-spec` + `builder-spec` + `architect-spec` bars |
54
60
  | solution-producer | `ownership`, the resolved `architect-spec` bar |
55
61
  | spec-judge | `spec-format`, `suite-format`, `lifecycle`, `gate-validation`, the resolved `oracle-spec` + `builder-spec` + `architect-spec` bars |
56
62
  | impl-producer | `ownership`, the resolved `builder-impl` + `architect-impl` bars |
@@ -31,6 +31,33 @@ producer is responding.
31
31
  artifact predates them. Any regression means the loop is **no longer converging**: stop, report
32
32
  it, and re-plan. Do not open another remediation round on a regressing loop.
33
33
 
34
+ ## The Clearance-repair proof — a repaired frozen scenario must fail its pre-repair draft
35
+
36
+ A `change` verdict that re-opens an **already-frozen** scenario under a **ratified Clearance re-open**
37
+ (a narrowing/rewrite of specified behavior, or an impl-gate Oracle-lens revert) carries one extra bar
38
+ on top of the four rules. The repair changes a contract that was already frozen, so the danger is a
39
+ **back-fit**: a "correction" reverse-engineered to fit whatever the current draft/implementation
40
+ already says, changing nothing of substance. The proof against that is directional —
41
+
42
+ > a genuine repair makes the **pre-repair** artifact **FAIL**; a contract narrowed to fit an existing
43
+ > draft moves *toward* passing it.
44
+
45
+ So the repair is re-approved only when the repaired scenario **fails when checked against the
46
+ pre-repair artifact/draft** — not merely that it passes against the post-repair one:
47
+
48
+ - a repaired scenario that **fails** the pre-repair artifact is **accepted** as a genuine contract
49
+ correction — the pre-repair failure is the evidence its substance changed;
50
+ - a repaired scenario that **already passes** the pre-repair artifact is **rejected** as a suspected
51
+ **back-fit** — it may be reverse-engineered from what already existed, not a real correction;
52
+ - a **post-repair pass with no demonstrated pre-repair failure is not enough** — a repair carrying no
53
+ pre-repair-failure proof is **not re-approved** (absence of the proof is not proof of substance).
54
+
55
+ The bar has **two faces**, both owed: the **producer** *demonstrates* the pre-repair failure as part
56
+ of the repair (run the repaired scenario against the pre-repair artifact and show it fails); the
57
+ **gate/judge** *requires* that proof before re-approving (a post-repair pass alone never re-approves).
58
+ An independent cold judge confirming the repaired scenario against the still-unrevised artifact is the
59
+ strongest form of the demonstration.
60
+
34
61
  ## A sweep is scope-aware, never a blanket match
35
62
 
36
63
  Rule 2's sweep answers "every instance of the rule", which is **not** "every occurrence of a string".
@@ -58,9 +85,13 @@ REMEDIATION:
58
85
  swept=<the other instances found, or none>
59
86
  ruled-out=<candidates inspected and excluded, with the reason>
60
87
  provenance=<pre-existing | regression>
88
+ pre-repair-proof=<the repaired scenario FAILS the pre-repair artifact | n/a — not a Clearance-gated frozen-scenario repair>
61
89
  ```
62
90
 
63
- A `contested` finding carries the evidence against it and **no edit** to the artifact it named.
91
+ A `contested` finding carries the evidence against it and **no edit** to the artifact it named. A
92
+ Clearance-gated repair of a frozen scenario carries its **`pre-repair-proof`** — the demonstration
93
+ that the repaired scenario fails the pre-repair artifact; a repair without it (or one that passes the
94
+ pre-repair draft) is not re-approved.
64
95
 
65
96
  ## Key points (read-check)
66
97
 
@@ -76,3 +107,7 @@ A `contested` finding carries the evidence against it and **no edit** to the art
76
107
  alone.
77
108
  6. **Provenance is derived from the diff** — an artifact changed by the previous round's commits
78
109
  makes its finding a **regression**, which stops the loop for a re-plan rather than another round.
110
+ 7. **A Clearance-gated repair of a frozen scenario must fail its pre-repair draft** — re-approval
111
+ requires the repaired scenario to **fail** against the pre-repair artifact (a repair that already
112
+ passes it is a suspected back-fit; a post-repair pass alone is not enough). The producer
113
+ demonstrates the failure; the gate/judge requires it.
@@ -19,8 +19,9 @@
19
19
  // Pure functions are exported for node:test; running the file directly drives the
20
20
  // CLI. No dependencies — plain node strips the types.
21
21
 
22
- import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs'
22
+ import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
23
23
  import { join } from 'node:path'
24
+ import { pathToFileURL } from 'node:url'
24
25
 
25
26
  // ─── the closed sets ───────────────────────────────────────────────────────────
26
27
 
@@ -512,4 +513,6 @@ export function main(argv: string[]): number {
512
513
  return 0
513
514
  }
514
515
 
515
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
516
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
517
+ process.exit(main(process.argv.slice(2)))
518
+ }
@@ -8,8 +8,8 @@ metadata:
8
8
 
9
9
  # Resolve Tracking
10
10
 
11
- The concrete engine for **tracking resolution** — the second escape-hatch trigger
12
- (`.agents/specs/sdd/intake/README.md`). For one touched artifact it decides **tracked** (SDD
11
+ The concrete engine for **tracking resolution** — the second escape-hatch trigger of the SDD
12
+ project spec's `intake` node (repo-only). For one touched artifact it decides **tracked** (SDD
13
13
  governs it — spec + gates) or **ignored** (SDD does not govern it; it still gets built) and
14
14
  reports which step decided it, so the conductor can skip a task outright (no CR, no draft, no
15
15
  gate, no record) when it resolves ignored. The split mirrors git's **tracked vs ignored** files.
@@ -1,9 +1,10 @@
1
1
  // resolve-tracking — resolve one artifact's tracking signal (tracked | ignored).
2
- // Self-contained, no deps (repo's node-≥23.6 convention). Spec:
3
- // .agents/specs/sdd/intake/resolve-tracking/README.md
2
+ // Self-contained, no deps (repo's node-≥23.6 convention). Spec: the intake/resolve-tracking
3
+ // node of the SDD project spec (repo-only).
4
4
 
5
- import { existsSync, readFileSync } from 'node:fs'
5
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
+ import { pathToFileURL } from 'node:url'
7
8
 
8
9
  export type Tracking = 'tracked' | 'ignored'
9
10
 
@@ -208,6 +209,6 @@ export function main(argv: string[]): void {
208
209
  process.stdout.write(`reason: ${result.reason}\n`)
209
210
  }
210
211
 
211
- if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
212
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
212
213
  main(process.argv.slice(2))
213
214
  }
@@ -17,7 +17,7 @@ Treat `$sdd`, "use SDD", and "use Spec-Driven Development" as explicit activatio
17
17
 
18
18
  ### Surface pending strategy
19
19
 
20
- When the Council re-enters, **surface the count of pending (unratified) strategy** — count the `strategy` lines with `"ratified": false` globbed from the project's **root `ledger/` shards** (the durable sibling directory of the root `spec.md`; a legacy `ledger.jsonl` still counts) and state "N pending strategy" alongside the intake; if the Council picks it, route them to review those entries. Count only `kind: strategy` — the conductor's `kind: leash` run-start blocks are **not** strategy and never counted. The gateway only *surfaces* the count — it never **drafts** strategy (the Scanner's job) nor **ratifies** it (the Council's positional act). A zero count is not surfaced. (`strategy` lives in the durable `ledger/` shards, **never** in the per-mission `*.log.jsonl` combat log.)
20
+ When the Council re-enters, **surface the count of pending (unratified) strategy** — count the `strategy` lines with `"ratified": false` **and `disposition: open`-or-absent** globbed from the project's **root `ledger/` shards** (the durable sibling directory of the root `spec.md`; a legacy `ledger.jsonl` still counts) and state "N pending strategy" alongside the intake; if the Council picks it, route them to review those entries. Count only `kind: strategy` — the conductor's `kind: leash` run-start blocks are **not** strategy and never counted, and a `disposition: resolved` line is the Scanner's validation **tombstone** (a cut, `sdd:combat-log-governance`), **never counted** even though it is `kind: strategy` with `"ratified": false`. The gateway only *surfaces* the count — it never **drafts** strategy (the Scanner's job) nor **ratifies** it (the Council's positional act). A zero count is not surfaced. (`strategy` lives in the durable `ledger/` shards, **never** in the per-mission `*.log.jsonl` combat log.)
21
21
 
22
22
  ### Surface in-progress missions
23
23
 
@@ -41,6 +41,11 @@ the edge alone: a scenario's `Given` is the path reaching the edge, its `When` i
41
41
  (`sdd:suite-format-governance`).
42
42
 
43
43
  - **1:1 scenario↔row** — every scenario has exactly one row, every row one scenario.
44
+ - **Name the scenario in backticks.** The `Scenario` cell holds the scenario's title **backtick-wrapped**
45
+ (`` `send text types literal text and presses no Enter` ``). This is how `check-suite` tells a data
46
+ row from the header and separator: a data row whose `Scenario` cell is **not** backtick-wrapped is
47
+ reported as an **unparseable row**, not silently skipped — a map that reads complete but binds nothing
48
+ is the exact gap the map exists to prevent.
44
49
  - **An edge may carry several rows.** That is **permutation coverage**, not duplication — legitimate
45
50
  when each row's path class yields a *different* outcome. Same edge *and* same path class twice is a
46
51
  duplicate.
@@ -109,8 +109,17 @@ Resolve the **spec-judge** for each `artifact-types` (a plugin judge or the SDD
109
109
  **{oracle, builder, architect}**. Then take the judge's **contract-sync verdict** (derived at this
110
110
  gate, never stored) and **derive the leash** (the conductor's autonomy bar,
111
111
  baked into `start-mission`) in-session. Collect the judge's `STATUS`,
112
- `ALIGNED`, failing scenarios, remaining `<!-- open: -->` markers, `OBSERVATIONS`, and the gate
113
- report. The judge is a **distinct cold actor** and never edits the artifact it grades.
112
+ `ALIGNED`, failing scenarios, remaining `<!-- open: -->` markers, `CONFORMANCE`, `OBSERVATIONS`, and
113
+ the gate report. The judge is a **distinct cold actor** and never edits the artifact it grades.
114
+
115
+ **A `CONFORMANCE.result: warn` is surfaced, never a block.** When the judge reports a spec-format
116
+ conformance warning (a touched **behavioral** `spec.md` missing a required section — especially
117
+ `## Use Cases`, `## Control Flow` / CFG, or `## Scenario map`), **surface it in the gate report** and
118
+ **do not** let it advance, block, or set `ALIGNED: false` on its own — it is a non-blocking finding
119
+ like `CONTENT_GAPS` or an introduced-reference finding, distinct from the deterministic structural
120
+ fail-closed checks and from a lens failure. The advance is governed by the lenses, the open markers,
121
+ and the alignment verdict exactly as before; the conformance warning rides alongside them in the
122
+ report.
114
123
 
115
124
  **Never advance** — by self-assertion or human verdict — with judge failures, any remaining open
116
125
  markers, or a misaligned suite. They fail the confidence dimension, so they forbid self-assertion
@@ -162,6 +171,10 @@ node "<skill>/scripts/classify-edit-class.mts" --files <the CR's touched .featur
162
171
 
163
172
  A `narrowing`/`mixed` result on a still-`@frozen` file routes to **Clearance** (escalated unless the CR
164
173
  pre-authorized it); `additive`/`no-content-change` self-clears; `unfrozen-skip` needs no edit-class gate.
174
+ When a Clearance re-open **repairs** an already-frozen scenario, re-approving the repair carries the
175
+ **pre-repair-failure proof** bar (`sdd:remediation-governance`): the repaired scenario must **fail**
176
+ against the pre-repair artifact — a repair that already passes it is a suspected back-fit, and a
177
+ post-repair pass alone never re-approves.
165
178
  `spec.md`
166
179
  / the node READMEs are **kept aligned, never frozen** — editable, but may not contradict a frozen
167
180
  scenario (enforced by the alignment check and the judge, not a flat freeze). Vocabulary is
@@ -192,6 +205,9 @@ nothing, advances no status, renders no verdict**. Fixed sections:
192
205
  ## Report
193
206
 
194
207
  - PASS / FAIL per lens, relayed from the judge
208
+ - **Spec-format conformance:** the judge's `CONFORMANCE` — on `warn`, a **warning** line naming each
209
+ missing required section (Use Cases / Control Flow / Scenario map) on the touched behavioral
210
+ `spec.md`; non-blocking, surfaced alongside the verdict, never a block on its own
195
211
  - `ALIGNED: true | false`; if false, which artifacts are out of sync
196
212
  - Open markers / failing scenarios still blocking, if any
197
213
  - The leash derivation and the effective leash for this gate
@@ -35,8 +35,9 @@
35
35
  // exported for node:test; running the file directly drives the CLI. No dependencies.
36
36
 
37
37
  import { execFileSync } from 'node:child_process'
38
- import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs'
38
+ import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
39
39
  import { basename, dirname, join } from 'node:path'
40
+ import { pathToFileURL } from 'node:url'
40
41
 
41
42
  export interface GateVerdict {
42
43
  verdict?: string
@@ -63,6 +64,12 @@ export interface LedgerGate {
63
64
  verdict: string
64
65
  }
65
66
 
67
+ // The lifecycle enum (lifecycle-governance), mirrored from discover-specs' recognition filter.
68
+ // Discovery *drops* a spec whose status is outside it, so every engine iterating discovered
69
+ // specs skips it silently; this check walks the tree itself, so it is the one place that still
70
+ // sees the file — and must escalate rather than exempt it (a status typo would otherwise remove
71
+ // a spec from all checking with no signal).
72
+ const LIFECYCLE_STATUSES = ['draft', 'approved', 'implemented', 'deprecated']
66
73
  const GATES = ['spec', 'impl']
67
74
  const VERDICTS = ['approve', 'pause', 'reject']
68
75
  const SPEC_TYPES = ['reference', 'behavioral']
@@ -132,6 +139,17 @@ export function checkSpec(slug: string, state: SpecState): string[] {
132
139
  const v: string[] = []
133
140
  const tag = (msg: string) => v.push(`${slug}: ${msg}`)
134
141
 
142
+ // The status must be classifiable before anything below it means anything: an
143
+ // out-of-enum (or absent) status makes every tuple rule below vacuous — the spec
144
+ // reads as neither approved nor implemented and passes silently, while discovery
145
+ // has already dropped it from every other engine. Unclassifiable is a failure.
146
+ if (!LIFECYCLE_STATUSES.includes(status))
147
+ tag(
148
+ status === ''
149
+ ? `no lifecycle status in frontmatter — a spec.md must declare status (${LIFECYCLE_STATUSES.join(' | ')}), and one that does not is checked by nothing`
150
+ : `status "${status}" is not in the lifecycle enum (${LIFECYCLE_STATUSES.join(' | ')}) — discovery drops it, so it is checked by nothing`,
151
+ )
152
+
135
153
  // `implemented` is backed by the impl gate's runtime suite run (ADR-0017), not a
136
154
  // stored flag — the static guard here is the recorded approval.impl ratification
137
155
  // (below). No `aligned` cross-check.
@@ -436,6 +454,11 @@ export function filterProseMdInSpecTree(paths: string[]): string[] {
436
454
  export interface UseCaseScenarioRefs {
437
455
  hasSection: boolean
438
456
  refs: string[]
457
+ // Trimmed text of each DATA row whose Scenario cell carries no backtick reference —
458
+ // missing, empty, or present-but-unparseable. Every one is surfaced as a violation
459
+ // rather than silently dropped: a row that names no scenario is a coverage gap, and
460
+ // exempting it is the fail-open shape this check exists to close.
461
+ unparseable: string[]
439
462
  }
440
463
 
441
464
  // A markdown table row: strip the leading/trailing `|` then split on `|`, trimming each cell.
@@ -465,26 +488,32 @@ export function extractUseCaseScenarioRefs(text: string): UseCaseScenarioRefs {
465
488
  // would erase the very refs this function extracts).
466
489
  const body = text.replace(/```[\s\S]*?```/g, '')
467
490
  const section = extractSection(body, 'Use Cases')
468
- if (section === null) return { hasSection: false, refs: [] }
491
+ if (section === null) return { hasSection: false, refs: [], unparseable: [] }
469
492
  const lines = section.split('\n')
470
493
  const headerIdx = lines.findIndex((l) => l.trim().startsWith('|'))
471
- if (headerIdx === -1) return { hasSection: true, refs: [] } // prose or EARS — no table
494
+ if (headerIdx === -1) return { hasSection: true, refs: [], unparseable: [] } // prose or EARS — no table
472
495
  const header = splitTableRow(lines[headerIdx])
473
496
  const scenarioIdx = header.findIndex((c) => /^scenario$/i.test(c))
474
- if (scenarioIdx === -1) return { hasSection: true, refs: [] } // table with no Scenario column
497
+ if (scenarioIdx === -1) return { hasSection: true, refs: [], unparseable: [] } // table with no Scenario column
475
498
 
476
499
  const refs: string[] = []
500
+ const unparseable: string[] = []
477
501
  // data rows start after the header separator (`|---|---|`); stop at the first
478
- // non-`|` line, which ends the contiguous table block.
502
+ // non-`|` line, which ends the contiguous table block. A data row whose Scenario
503
+ // cell carries no backtick reference is collected as unparseable — never silently
504
+ // skipped, which would fail this coverage check open.
479
505
  for (let i = headerIdx + 2; i < lines.length; i++) {
480
506
  if (!lines[i].trim().startsWith('|')) break
481
507
  const cells = splitTableRow(lines[i])
482
- const cell = cells[scenarioIdx]
483
- if (!cell) continue
508
+ // A missing cell (a row shorter than the header) and an empty one are the same
509
+ // defect as an un-backticked one: the row names no covering scenario. All three
510
+ // are collected — skipping any of them fails this coverage check open.
511
+ const cell = cells[scenarioIdx] ?? ''
484
512
  const ref = /`([^`\n]+)`/.exec(cell)
485
513
  if (ref) refs.push(ref[1].trim())
514
+ else unparseable.push(lines[i].trim())
486
515
  }
487
- return { hasSection: true, refs }
516
+ return { hasSection: true, refs, unparseable }
488
517
  }
489
518
 
490
519
  function escapeRegExp(s: string): string {
@@ -520,8 +549,13 @@ export function findSiblingFeature(dir: string): string | null {
520
549
  export function checkUseCaseCoverage(slug: string, dir: string, text: string): string[] {
521
550
  const v: string[] = []
522
551
  const tag = (msg: string) => v.push(`${slug}: ${msg}`)
523
- const { hasSection, refs } = extractUseCaseScenarioRefs(text)
524
- if (!hasSection || refs.length === 0) return v
552
+ const { hasSection, refs, unparseable } = extractUseCaseScenarioRefs(text)
553
+ if (!hasSection) return v
554
+
555
+ for (const raw of unparseable) {
556
+ tag(`Use Cases data row has no backtick-wrapped Scenario cell — ${raw}`)
557
+ }
558
+ if (refs.length === 0) return v
525
559
 
526
560
  const featurePath = findSiblingFeature(dir)
527
561
  const featureText = featurePath ? readFileSync(featurePath, 'utf8') : ''
@@ -598,4 +632,6 @@ export function main(argv: string[]): number {
598
632
  return 0
599
633
  }
600
634
 
601
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
635
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
636
+ process.exit(main(process.argv.slice(2)))
637
+ }
@@ -3,8 +3,9 @@
3
3
  // scenario ordering/sectioning checks. Pure functions are exported for node:test;
4
4
  // running the file directly drives the CLI.
5
5
 
6
- import { type Dirent, readdirSync, readFileSync } from 'node:fs'
6
+ import { type Dirent, readdirSync, readFileSync, realpathSync } from 'node:fs'
7
7
  import { basename, dirname, join } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
8
9
  import { validateFeatures } from 'gherkin-cli'
9
10
 
10
11
  // ─── types ────────────────────────────────────────────────────────────────────
@@ -403,46 +404,79 @@ export interface MapRow {
403
404
  scenario: string
404
405
  }
405
406
 
406
- export function parseScenarioMap(specText: string): MapRow[] | undefined {
407
- const start = specText.indexOf('## Scenario map')
408
- if (start === -1) return undefined
409
- const body = specText.slice(start)
407
+ export interface ScenarioMap {
408
+ rows: MapRow[]
409
+ // Trimmed text of each DATA row whose Scenario cell is not backtick-wrapped — a
410
+ // present-but-unparseable row, surfaced as a violation rather than silently dropped.
411
+ unparseable: string[]
412
+ }
413
+
414
+ // Parse the `## Scenario map` section's tables. The map is grouped by use case, so the section
415
+ // holds one or more markdown tables — a `###` sub-header or a blank line breaks a table block.
416
+ // Within each contiguous `|`-row block the first row is the column header and the second the dashed
417
+ // separator (recognized POSITIONALLY); every row after those is a DATA row whose Scenario cell
418
+ // (column 3) must be backtick-wrapped. A data row that is not is collected in `unparseable`: the
419
+ // backtick match discriminates a data row's cell, it is never the sole signal that a row exists —
420
+ // conflating the two silently dropped a fully-authored-but-un-backticked map (the fail-open closed
421
+ // here). The section is bounded at the next `## ` heading so a following section's table (e.g.
422
+ // `## References`) is not misread as map rows.
423
+ export function parseScenarioMap(specText: string): ScenarioMap | undefined {
424
+ // Anchor to a real `## Scenario map` HEADING line — not a mid-line or backtick-quoted prose
425
+ // mention of the string (a spec that documents the map must not be misread as having one).
426
+ const heading = /^## Scenario map[ \t]*$/m.exec(specText)
427
+ if (heading === null) return undefined
428
+ const after = specText.slice(heading.index + heading[0].length)
429
+ const nextHeading = after.search(/\n## /)
430
+ const body = nextHeading === -1 ? after : after.slice(0, nextHeading)
431
+
410
432
  const rows: MapRow[] = []
433
+ const unparseable: string[] = []
434
+ let blockRow = -1 // index within the current contiguous |-row block; -1 = not in a block
411
435
  for (const line of body.split('\n')) {
412
436
  const t = line.trim()
413
- if (!t.startsWith('|')) continue
437
+ if (!t.startsWith('|')) {
438
+ blockRow = -1 // any non-table line ends the current block
439
+ continue
440
+ }
441
+ blockRow++
442
+ if (blockRow < 2) continue // this block's header (0) and dashed separator (1)
414
443
  const cells = t
415
444
  .split('|')
416
445
  .slice(1, -1)
417
446
  .map((c) => c.trim())
418
- if (cells.length !== 3) continue
419
- const scenario = cells[2] ?? ''
420
- // Skip the header row and its separator; a data row names its scenario in backticks.
447
+ const scenario = cells.length === 3 ? (cells[2] ?? '') : ''
421
448
  const m = scenario.match(/^`(.+)`$/)
422
- if (m === null) continue
449
+ if (m === null) {
450
+ unparseable.push(t)
451
+ continue
452
+ }
423
453
  rows.push({ edge: cells[0] ?? '', path: cells[1] ?? '', scenario: m[1] ?? '' })
424
454
  }
425
- return rows
455
+ return { rows, unparseable }
426
456
  }
427
457
 
428
458
  export function checkScenarioMap(slug: string, file: string, featureText: string, specText: string): string[] {
429
- const rows = parseScenarioMap(specText)
430
- if (rows === undefined) return []
459
+ const map = parseScenarioMap(specText)
460
+ if (map === undefined) return []
431
461
  const tag = (msg: string) => `${slug}/${file}: ${msg}`
432
462
  const v: string[] = []
433
463
 
464
+ for (const raw of map.unparseable) {
465
+ v.push(tag(`scenario-map data row has no backtick-wrapped Scenario cell — ${raw}`))
466
+ }
467
+
434
468
  const titles = [...featureText.matchAll(/^\s*Scenario(?: Outline)?:\s*(.+?)\s*$/gm)].map((m) => m[1] ?? '')
435
- const mapped = new Set(rows.map((r) => r.scenario))
469
+ const mapped = new Set(map.rows.map((r) => r.scenario))
436
470
 
437
471
  for (const t of titles) {
438
472
  if (!mapped.has(t)) v.push(tag(`scenario is not on the scenario map — "${t}"`))
439
473
  }
440
474
  const titleSet = new Set(titles)
441
- for (const r of rows) {
475
+ for (const r of map.rows) {
442
476
  if (!titleSet.has(r.scenario)) v.push(tag(`scenario map row names no such scenario — "${r.scenario}"`))
443
477
  }
444
478
  const seen = new Map<string, string>()
445
- for (const r of rows) {
479
+ for (const r of map.rows) {
446
480
  const key = `${r.edge}\u0000${r.path}`
447
481
  const prior = seen.get(key)
448
482
  if (prior !== undefined) {
@@ -498,4 +532,6 @@ export function main(argv: string[]): number {
498
532
  return 0
499
533
  }
500
534
 
501
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
535
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
536
+ process.exit(main(process.argv.slice(2)))
537
+ }
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // classify-edit-class — the structural edit-class classifier for a touched frozen `.feature`
3
- // (`.agents/specs/sdd/authoring/spec-gate/README.md` — "Structural edit-class classification
4
- // (freeze integrity)"). Classifies each touched file's change against its committed baseline so
5
- // the gate can route: additive / no-content-change self-clear; narrowing / mixed take the
6
- // existing narrowing -> Clearance path. This engine only CLASSIFIES — it fires no verdict and
7
- // adds no new floor.
3
+ // (the "Structural edit-class classification (freeze integrity)" section of the SDD project spec's
4
+ // `authoring/spec-gate` node, repo-only). Classifies each touched file's change against its
5
+ // committed baseline so the gate can route: additive / no-content-change self-clear; narrowing /
6
+ // mixed take the existing narrowing -> Clearance path. This engine only CLASSIFIES — it fires no
7
+ // verdict and adds no new floor.
8
8
  //
9
9
  // The classification is STRUCTURAL, never a raw git line-diff. A raw line-diff is fooled by a
10
10
  // trailing step orphaned off a frozen scenario onto a newly added adjacent scenario: the orphan
@@ -33,8 +33,9 @@
33
33
  // Pure functions are exported for node:test; running the file directly drives the CLI.
34
34
 
35
35
  import { execFileSync } from 'node:child_process'
36
- import { readFileSync } from 'node:fs'
36
+ import { readFileSync, realpathSync } from 'node:fs'
37
37
  import { dirname, join, relative, resolve, sep } from 'node:path'
38
+ import { pathToFileURL } from 'node:url'
38
39
  import { type DiffReader, diffFeatures, GitError } from 'gherkin-cli'
39
40
 
40
41
  // ─── types ────────────────────────────────────────────────────────────────────
@@ -408,4 +409,6 @@ export function main(argv: string[]): number {
408
409
  return 0
409
410
  }
410
411
 
411
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
412
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
413
+ process.exit(main(process.argv.slice(2)))
414
+ }
@@ -32,7 +32,7 @@ USER_ANSWERS: <answers to previously returned QUESTIONS — or null>
32
32
 
33
33
  3. **Write the `spec.md` body per `sdd:spec-format-governance`.** That bar owns the required structure — the `## Use Cases` section (subject, non-goals, and the entry-point table of trigger / inputs / outcome) and the enrichment rules; follow it rather than re-listing sections here (a hardcoded list drifts from the bar). Author the body content — What, Why, design decisions, and the command / API surface where one exists — and enrich for human review (headings, tables, short paragraphs, a diagram where it carries the idea). Never leave placeholders (`TBD`, `TODO`, empty sections). **Do not** write the control frontmatter (`status`, `project-path`, `approval`, `produced-by`) — those belong to the conductor and the gate skill. Every referenced engine, skill, or artifact path you name must be real — a reference that resolves to nothing is caught mechanically at step 5 below, but naming a real path the first time spends no round on it. **On `BACKFILL` the four sections are still mandatory** — draw the `## Control Flow` CFG and its `## Scenario map` from the code, never stop at `## Use Cases` (`sdd:spec-format-governance`; `check-spec-structure`'s `incomplete-node` flags a leaf that skips them).
34
34
 
35
- 4. **Write `<DOMAIN_PATH>/<DOMAIN>.feature`** — pure boolean Gherkin per `sdd:suite-format-governance`. **Cover every use case from the `## Use Cases` section with one-or-more scenarios** (happy path, negative mirror, boundary) — a use case with no scenario is unverified intent; a scenario with no use case is an orphan. **On `BACKFILL`, re-derive the scenario set from the CFG's edges** rather than patching the standing suite; the retired corpus is **reference only**, a claim to verify against the current code (`sdd:suite-format-governance`). Each `Then` is an observable boolean — name the artifact a verifier reads to settle it; an act is assertable only when it leaves a trace, and where it records nothing, add the record rather than dropping the act. Never internal state, function names, "sometimes", or how the artifact was authored. Order scenarios by lifecycle stage (the step-down convention). Keep the `.feature` plain; rubric form is legal only inside an `@rubric`-tagged scenario.
35
+ 4. **Write `<DOMAIN_PATH>/<DOMAIN>.feature`** — pure boolean Gherkin per `sdd:suite-format-governance`. **For a fold (aggregation) node whose rule combines two or more *interacting* sub-conditions, state that rule in closed form — and re-derive its soundness against the real data model — _before_ you derive any scenario** (`sdd:suite-format-governance`). The **order is load-bearing**: scenarios are drawn *from* the rule, so deriving them first is the retrofit-after-the-fact shape that diverged (`github-192`, by example) where stating the rule first converged (`github-224`). A **single-condition** fold may be specified by example, and demanding a closed form of it is **over-firing** — the failure mode of this rule. Closed form is not soundness (`R''` shipped a proof and still deadlocked until re-derived against the real graph, `R'''`), and it buys **convergence, not coverage** — pair the rule with a **mutation sweep** (each interacting condition's mutation breaks a distinct scenario) and a **safety dual** (a liveness scenario passes an over-permissive fold green; assert the case it cannot observe). A **matrix / per-cell** claim is the same rule applied — draw every independent cell as its own scenario, exclude the degenerate cells, and confirm the cells distinct by the sweep. **Cover every use case from the `## Use Cases` section with one-or-more scenarios** (happy path, negative mirror, boundary) — a use case with no scenario is unverified intent; a scenario with no use case is an orphan. **On `BACKFILL`, re-derive the scenario set from the CFG's edges** rather than patching the standing suite; the retired corpus is **reference only**, a claim to verify against the current code (`sdd:suite-format-governance`). Each `Then` is an observable boolean — name the artifact a verifier reads to settle it; an act is assertable only when it leaves a trace, and where it records nothing, add the record rather than dropping the act. Never internal state, function names, "sometimes", or how the artifact was authored. Order scenarios by lifecycle stage (the step-down convention). Keep the `.feature` plain; rubric form is legal only inside an `@rubric`-tagged scenario.
36
36
 
37
37
  **A `Given` is a test vector, not specification** (`sdd:suite-format-governance` carries the canonical bar and the swap test). Author each `Given`'s apparatus — its domain, entities, names, framing — from a domain **the artifact does not illustrate**. On a revise CR the apparatus never reuses the artifact's existing worked examples; on `BACKFILL` it never reuses the illustrations you read out of source. Read those examples in full at step 1 — they are evidence of the behavior you are specifying; exclude them only from the apparatus you author into a `Given`.
38
38
 
@@ -46,7 +46,7 @@ USER_ANSWERS: <answers to previously returned QUESTIONS — or null>
46
46
 
47
47
  **A clean form check does not clear an entangled `Given`.** The engine reads form, not apparatus — it reports no violation on a `Given` whose apparatus reuses the artifact's worked examples. Re-read each authored `Given` against the test-vector bar by hand and rewrite the apparatus before returning `STATUS: complete`.
48
48
 
49
- **A clean form check does not clear a scenario that cannot fail.** The engine reads form, not discrimination — a well-formed `Then`, and a well-formed `@rubric` block, are both reported clean when no subject can ever fail them. Apply the **miss test** (`sdd:suite-format-governance`) to every scenario and every `@rubric` dimension you authored: name a **plausible wrong subject** — a memorizer, a copier, a procedure-follower, a single-brancher — and check that it *loses*. Name none and the scenario is inert; rewrite it. The wrong subject must be plausible — an empty artifact fails everything and clears nothing. Rewrite any dimension grading **presence** (a line is emitted), **restatement** (the doctrine's own words), or **procedure** (the steps, not the judgment). Rewrite the **toothless finding** — a `Then` asserting a signal or finding is *raised* but not its **binding consequence** (it withholds the pass, blocks the gate, changes the outcome): name the wrong subject that raises the finding and acts on nothing, then rewrite the `Then` to assert the consequence. Rewrite the **process-`Then`** — a `Then` asserting **how** the artifact was produced ("co-developed with the code", "written test-first", "refactored before completing", authoring order) rather than its **observable behavior or end-state**; nothing in the artifact or a run reveals the authoring sequence, so it can never be a `Then` — rewrite it to assert the observable behavior instead (`sdd:suite-format-governance`). Before writing any criterion as a dimension, run the **substitutability test** (`sdd:suite-format-governance`): a criterion belongs in a `@rubric` **only if** you accept that strength elsewhere may pay for weakness here — otherwise it is not in the sum at all, it is a boolean `Then`. **Write the trade down** for each dimension you author or revise, in the same record that carries the cut's reason, naming it and **what pays for it** — an unrecorded trade is an unowned selection nobody can disagree with. The duty is **yours alone**: no judge reports a missing record, so nothing catches you skipping it. For a `@rubric`, sum what each named wrong subject **banks** (never zero a dimension to make a point): that sum sits **strictly under** the threshold — a tie passes, since the collapsing `Then` passes a score *at least* the threshold. How far under is **not** a constant: it is your judge's noise at the cut (**cSEM**), measured by scoring the subject more than once, never decreed here.
49
+ **A clean form check does not clear a scenario that cannot fail.** The engine reads form, not discrimination — a well-formed `Then`, and a well-formed `@rubric` block, are both reported clean when no subject can ever fail them. Apply the **miss test** (`sdd:suite-format-governance`) to every scenario and every `@rubric` dimension you authored: name a **plausible wrong subject** — a memorizer, a copier, a procedure-follower, a single-brancher — and check that it *loses*. Name none and the scenario is inert; rewrite it. The wrong subject must be plausible — an empty artifact fails everything and clears nothing. Rewrite any dimension grading **presence** (a line is emitted), **restatement** (the doctrine's own words), or **procedure** (the steps, not the judgment). Rewrite the **toothless finding** — a `Then` asserting a signal or finding is *raised* but not its **binding consequence** (it withholds the pass, blocks the gate, changes the outcome): name the wrong subject that raises the finding and acts on nothing, then rewrite the `Then` to assert the consequence. Rewrite the **process-`Then`** — a `Then` asserting **how** the artifact was produced ("co-developed with the code", "written test-first", "refactored before completing", authoring order) rather than its **observable behavior or end-state**; nothing in the artifact or a run reveals the authoring sequence, so it can never be a `Then` — rewrite it to assert the observable behavior instead (`sdd:suite-format-governance`). Before writing any criterion as a dimension, run the **substitutability test** (`sdd:suite-format-governance`): a criterion belongs in a `@rubric` **only if** you accept that strength elsewhere may pay for weakness here — otherwise it is not in the sum at all, it is a boolean `Then`. **Write the trade down** for each dimension you author or revise, in the same record that carries the cut's reason, naming it and **what pays for it** — an unrecorded trade is an unowned selection nobody can disagree with. The duty is **yours alone**: no judge reports a missing record, so nothing catches you skipping it. For a `@rubric`, sum what each named wrong subject **banks** (never zero a dimension to make a point): that sum sits **strictly under** the threshold — a tie passes, since the collapsing `Then` passes a score *at least* the threshold. How far under is **not** a constant: it is your judge's noise at the cut (**cSEM**), measured by scoring the subject more than once, never decreed here. **Ground a dimension or its cut on non-author evidence (cold-instrument doctrine).** When you justify a `@rubric` dimension or its cut with a **measurement** — an ablation Δ, a discrimination count over N runs — it is admissible only if it is **not solely your own**: it meets the non-author evidence standard the doctrine Strategist states canonically (`sdd:doctrine-loop`), never re-listed here. Your own instrument silently assumes the property under test, so a cut grounded on your own measurement alone is **not grounded** — record that it needs non-author or fresh-adversarial evidence. A measurement grounding no dimension or cut is unconstrained; this governs the *evidence*, not the cut *value* (that is the miss-test arithmetic above).
50
50
 
51
51
  **In `revise` mode, run the substitutability test over the standing `@rubric` dimensions your CR touches, not only the ones you author.** A dimension already in the sum that fails the test is a **correction**, and a correction is not a deletion: removing it changes the attainable maximum, so the cut it leaves behind is **un-re-derived whether or not its number still needs to change**. Re-derive that cut as a fresh policy call **in the same edit** and record its reason against the new attainable maximum; raise **one clearance per corrected scenario** and never let one blanket approval stand in for each scenario's own cut decision. Route it on the **removal** — never on whether the diff calls the edit `mixed`, which the in-scenario shape is not. The full procedure is *Correcting a standing rubric* (`sdd:suite-format-governance`). A green form check does **not** clear this: `check-suite.mts` reports only the vacuous `sum(max) < threshold` rubric, and a cut nobody re-derived clears that check every time.
52
52
 
@@ -14,11 +14,9 @@ diffs. It **decides** the cut; it does not build, store, classify, or automatica
14
14
  decision-evidence (SQ-F5 #194, deferred).
15
15
 
16
16
  Built for the Op2 ★ capstone of the cyberfleet-batch change request (GitHub issue #189, the reasoning
17
- front-end above the shipped deterministic back-end); see
18
- [`.agents/specs/sdd/ssa-lowering/README.md`](../../../../.agents/specs/sdd/ssa-lowering/README.md) for
19
- the authoritative behavior description and
20
- [`ssa-lowering.feature`](../../../../.agents/specs/sdd/ssa-lowering/ssa-lowering.feature) for the frozen
21
- behavior suite.
17
+ front-end above the shipped deterministic back-end); the `ssa-lowering` node of the SDD project spec
18
+ (in the cyberplace repository, not shipped in this package) carries the authoritative behavior
19
+ description and the frozen behavior suite.
22
20
 
23
21
  This is a **doctrine, not an engine** — it emits no `.mts`, computes nothing deterministically, and holds
24
22
  no state. Working node name only (SQ-name #195).
@@ -41,7 +41,7 @@ For each unit the CR touches:
41
41
  - **Scaffold the skeleton** per `sdd:spec-format-governance` (sections per type; `.feature` form per `sdd:suite-format-governance`). Write **no** control frontmatter (`status` / `project-path` / `approval` / `produced-by`) — those live on the root `spec.md` and belong to the conductor and the gate.
42
42
  - **Collect seed intent.** For a **new** feature, ask 3–5 targeted questions (the core problem and who has it; observable behavior; the public interface; edge cases / non-goals; reviewers who must be heard). For **backfill** (behavior already in code), skip — the producer reads source, tests, history. For a **revise**, collect what changes and why and the parts it touches.
43
43
 
44
- **The grill loop (the user loop).** You are the conductor. Run the spec-producer **inline** (load `sdd:spec-producer-governance`, or persona-load a plugin specialist for the `artifact-types`), **dispatch the cold spec-judge** each round — through the dispatch capability's intent seam when one is available (preferring its **warm** unit, context-cleared fresh via `npx cyberlegion@<version> unit clear <ref>` before each round's judgment), else a portable cold subagent — and for build-to-learn **dispatch the impl-producer builder** the same way (its warm unit **keeps** its context across spikes; no reset) in `explore` mode against the **non-frozen** suite — spikes are thrown away; their learnings feed the live grill to steer the spec + suite. Set an **iteration cap** (default **3**; honor a user-named cap), then loop:
44
+ **The grill loop (the user loop).** You are the conductor. Run the spec-producer **inline** (load `sdd:spec-producer-governance`, or persona-load a plugin specialist for the `artifact-types`), **dispatch the cold spec-judge** each round — through the dispatch capability's intent seam when one is available (preferring its **warm** unit, context-cleared fresh via `npx cyberlegion@0.2.0 unit clear <ref>` before each round's judgment), else a portable cold subagent — and for build-to-learn **dispatch the impl-producer builder** the same way (its warm unit **keeps** its context across spikes; no reset) in `explore` mode against the **non-frozen** suite — spikes are thrown away; their learnings feed the live grill to steer the spec + suite. Set an **iteration cap** (default **3**; honor a user-named cap), then loop:
45
45
 
46
46
  **Governance provenance relay.** When you dispatch the cold spec-judge, forward the inline spec-producer's declared `governances_loaded` (`sdd:spec-producer-governance`) verbatim through the same dispatch channel, keyed **`producer_governances_declared`** — a brief field when the judge is a cold subagent, a mail envelope field when it runs through an agent pool. Forward it **as-is, including an empty set** — you render **no opinion** on which governances were actually required; that check is the spec-judge's own pre-flight (`sdd:sdd-spec-judge`).
47
47
 
@@ -79,7 +79,7 @@ Build-to-keep against the **frozen** suite. The deliver **read-set is scoped** (
79
79
 
80
80
  **Rebase onto the target — the last deliver act, before the gate.** Before running the impl gate, **rebase the CR branch onto the current tip of the declared target** (for a commit-to-main project, the equivalent `pull --rebase` onto the latest `main`), so the impl gate judges the **merged tree that will actually land** — keeping history linear and leaving handoff a pure consumer that never re-verifies. A **textual conflict** is resolved as **deliver code work** against the frozen `.feature` (never a `.feature` edit); the gate then runs on the resolved tree. A conflict you **cannot resolve confidently is never guess-resolved** — the frozen suite covers *this CR's* behavior, not the incoming change's, so a wrong resolution could still pass the gate and land broken; **stop and escalate** (in-session ask the user; headless return `needs-input` up the relay) and record a `halt`, never land a low-confidence resolution. Rebasing an *unmerged* CR branch is git-reversible (reflog), so it raises **no new hard floor** — but a conflict resolution that would **narrow** a frozen scenario still fires the existing **Clearance** floor, a semver class over the ceiling **Compatibility**, and a genuine contradiction **Conflict** (autonomy bar, below). The rebase-then-gate is **optimistic**: if the target **advances again** between the passing gate and the push (another CR merged in the window), **re-rebase onto the new tip and re-run the impl gate — do not push until the gate passes on the re-rebased tree**, looping until the push wins, so what lands is always a tree the gate saw green. **The loop is bounded, not forced** — if the target keeps advancing past a small cap of attempts, **stop and escalate** (record a `halt`) rather than spinning forever (a liveness stop, same as the unconfident-conflict halt).
81
81
 
82
- **The impl gate (Approved → Implemented, internal).** On entering the gate, overwrite the statusline file with `impl gate`. Dispatch the **cold impl-judge** (`sdd:sdd-impl-judge` or the covering plugin's judge) — same seam-when-available wiring, preferring its warm unit context-cleared fresh via `npx cyberlegion@<version> unit clear <ref>` for this judgment, else a portable cold subagent — to run the verification per frozen scenario plus an orthogonal structural/scope read. Advance to **`status: implemented`** **only when every impl-judge passes** (a frozen scenario with no verification blocks the advance — impl-sync is this suite run, not a stored flag). The three actions: **approve** → `implemented`; **change** → fix the **code** (never the frozen `.feature`), under the same evidence-not-a-work-order remediation the spec gate uses (`sdd:remediation-governance`) — including the **provenance** account that stops a regressing loop; **reject** → redo, or a **Oracle-lens revert** (a frozen scenario proved fatal → unfreeze the `.feature`, return to `draft` — the only place a frozen `.feature` reopens).
82
+ **The impl gate (Approved → Implemented, internal).** On entering the gate, overwrite the statusline file with `impl gate`. Dispatch the **cold impl-judge** (`sdd:sdd-impl-judge` or the covering plugin's judge) — same seam-when-available wiring, preferring its warm unit context-cleared fresh via `npx cyberlegion@0.2.0 unit clear <ref>` for this judgment, else a portable cold subagent — to run the verification per frozen scenario plus an orthogonal structural/scope read. Advance to **`status: implemented`** **only when every impl-judge passes** (a frozen scenario with no verification blocks the advance — impl-sync is this suite run, not a stored flag). The three actions: **approve** → `implemented`; **change** → fix the **code** (never the frozen `.feature`), under the same evidence-not-a-work-order remediation the spec gate uses (`sdd:remediation-governance`) — including the **provenance** account that stops a regressing loop; **reject** → redo, or a **Oracle-lens revert** (a frozen scenario proved fatal → unfreeze the `.feature`, return to `draft` — the only place a frozen `.feature` reopens).
83
83
 
84
84
  ## Step 4 — handoff
85
85
 
@@ -95,17 +95,21 @@ Land per the handoff unit. First **finalize placement**: run a Warden placement
95
95
 
96
96
  Before you close out, run the **correction-line finalize backstop** (autonomy bar, below): flush any correction whose combat-log line was never written, creating the plan's `*.log.jsonl` if absent.
97
97
 
98
- Before closing out, **reset the mission's warm units**: `npx cyberlegion@<version> unit clear <ref>` (context-clear, pane stays warm) or tear down every warm unit this mission dispatched — none carries this mission's context into the next.
98
+ Also run the **plan-brief finalize backstop** (autonomy bar, below): reconcile the plan brief's `todos` and its `## NEXT` anchor to the landed state, **in this same change** — so the delivery never ships a landed mission described as in-progress.
99
+
100
+ Before closing out, **reset the mission's warm units**: `npx cyberlegion@0.2.0 unit clear <ref>` (context-clear, pane stays warm) or tear down every warm unit this mission dispatched — none carries this mission's context into the next.
99
101
 
100
102
  Once landed, **do not spawn** the formation Warden. Surface a **one-line nudge** that a corpus-wide formation pass is due, pointing to `sdd:manage` ("audit the corpus structure" → `formation-loop`). The pass is **on-demand** — run deliberately, not auto-spawned on every landing; `sdd:manage` owns the trigger. Gate nothing on it.
101
103
 
102
104
  ## Autonomy, provenance, and the hard floor (baked in)
103
105
 
104
- - **Dispatch transport.** Every spawn beyond this session states a **dispatch intent** — role, brief, expected verdict schema — never a pinned command. When a harness-agnostic dispatch capability is available (detected at runtime; the concrete case is the Legate's `dispatch-governance` composing `cyberlegion` primitives — `agent resolve` + `unit spawn` + `mail await` — with **no** `dispatch` CLI verb, the seam named in `.agents/specs/sdd/design/harness-spawning.md`), route through its intent seam and let it pick `subagent | channel | run-inline`, **preferring a warm unit** over a cold one-shot spawn; with no capability present, fall back to the portable cold subagent (depth-1) default — grader independence intact either way. **Warmth is a property of the unit/process; coldness of the context**: a judge's fresh-context guarantee (ADR-0016) is transport-agnostic — satisfied by a newly spawned cold subagent **or** a warm unit **context-cleared** to a fresh context before **each** judgment (re-deriving its oracle, carrying none of a prior round's context). Clear a warm unit with **`npx cyberlegion@<version> unit clear <ref>`** (`<ref>` = unit id / handle / worktree branch or CR ref) — it injects the harness's own fresh-context command (`/clear` on Claude/Codex/Copilot, `/new-chat` on Cursor; fail-loud on a harness with no honest reset) so the **pane stays warm** while the **context goes cold**; it tears nothing down. The **impl-producer builder** instead stays warm and **keeps** its context across the explore spikes and the deliver build (never cleared between those uses). Warm units stay warm for **one mission** — reused within it, then **`unit clear`**'d or torn down at **handoff**, never carrying this mission's context into the next.
106
+ - **Dispatch transport.** Every spawn beyond this session states a **dispatch intent** — role, brief, expected verdict schema — never a pinned command. When a harness-agnostic dispatch capability is available (detected at runtime; the concrete case is the Legate's `dispatch-governance` composing `cyberlegion` primitives — `agent resolve` + `unit spawn` + `mail await` — with **no** `dispatch` CLI verb, the seam named in the SDD project spec's `design/harness-spawning` node, repo-only), route through its intent seam and let it pick `subagent | channel | run-inline`, **preferring a warm unit** over a cold one-shot spawn; with no capability present, fall back to the portable cold subagent (depth-1) default — grader independence intact either way. **Warmth is a property of the unit/process; coldness of the context**: a judge's fresh-context guarantee (ADR-0016) is transport-agnostic — satisfied by a newly spawned cold subagent **or** a warm unit **context-cleared** to a fresh context before **each** judgment (re-deriving its oracle, carrying none of a prior round's context). Clear a warm unit with **`npx cyberlegion@0.2.0 unit clear <ref>`** (`<ref>` = unit id / handle / worktree branch or CR ref) — it injects the harness's own fresh-context command (`/clear` on Claude/Codex/Copilot, `/new-chat` on Cursor; fail-loud on a harness with no honest reset) so the **pane stays warm** while the **context goes cold**; it tears nothing down. The **impl-producer builder** instead stays warm and **keeps** its context across the explore spikes and the deliver build (never cleared between those uses). Warm units stay warm for **one mission** — reused within it, then **`unit clear`**'d or torn down at **handoff**, never carrying this mission's context into the next.
105
107
  - **Initial strategy** (run start): assess blast radius + the other dimensions and emit a run-level `kind: leash` block to **your own ledger shard** (`ledger/<cr-ref>.<hash>.jsonl` — mint `<hash>` as 6 random hex **once per session** and reuse it for every line you append; `sdd:combat-log-governance`) — `leash` (`auto-none | auto-spec | auto-all`), `by: derived | user`, `approach[]`. It may be user-specified. This block is `kind: leash`, **not** `strategy` — `strategy` is the doctrine Scanner's alone. Ledger lines carry **no `ts`**.
106
108
  - **Per-gate verdict.** At each gate, derive the leash against discovered state and either **self-assert within leash** (write `approval.<gate>: { verdict: approve, by: agent, why }`; the spec lands in the async review queue) or **stop** with a verdict packet for the human. **Never advance** when any judge fails, any open marker remains, or (at the impl gate) any frozen scenario's verification does not pass. Human ratification (`by: <name>`, advance `status`) is reserved to the in-session position holding the user channel — by default you, in-session; a headless `automaton` emits the verdict packet and stops, **even when a coordinator relays "the user approved."**
107
109
  - **Combat log.** Append `report` / `correction` lines (and the halt that stopped you) to the plan's `*.log.jsonl` (these carry a UTC `ts`); your run-start `leash` block, self-asserted `gate` lines, and the handoff `followup` records go to **your own shard** in the durable `ledger/` directory sibling to `spec.md` — never another writer's shard, never a shared file (`strategy` there is the Scanner's alone). Free text is commit-message-grade — never code, prompts, secrets, or literal values.
108
110
  - **Correction-line durability** (`combat-log-governance` write duty). When a gate you self-assert was reached via a **judge-reject→fix→pass**, append the discrete `correction` line (`correction-kind: judge-iteration`, a matchable `cause`) to the combat log **before** you write the gate `why` — never leave the iteration recorded only in the `why` prose; a gate that passed clean appends none. At **handoff/finalize**, if any correction occurred whose combat-log line was never flushed, write it now — **creating the plan's `*.log.jsonl` if it does not exist** — so no correction is lost to the no-log mission class (a mission with no correction forces nothing). The forced line stays a combat-log `correction`, never a ledger line.
111
+ - **Cause-enum conformance** (`combat-log-governance` write duty). Every time you write a `cause` — a `correction`'s matchable cause **or** a `gate` line's stop cause — **prefer an enum value; if none fits, write the off-enum string into `cause` anyway and flag the line `cause-candidate: true`**, so it stays countable as a proposed enum-growth value instead of silently failing closed. A **visibility nudge, never a write-blocking linter** — the write always succeeds, and forcing an ill-fitting enum value would only relabel the silent drop. An **absent** `cause` still fails closed (the nudge governs only the *no-value-fits* case and licenses no omission). The `gate` stop-cause enum is `dimension | clearance | ceiling`; the `correction` matchable-cause enum is `coverage-gap | design-overreach | spec-feature-contradiction | prose-impl-contradiction` — both closed sets grown only by Council ratification, and a recurring `cause-candidate` value is the growth signal the Council reads.
112
+ - **Plan-brief durability** (the execution-state sibling of the correction-line durability rule above). At **handoff/finalize**, a mission that **lands** reconciles its plan brief to the landed state — every todo set to its true terminal state, and the `## NEXT` anchor rewritten to say what landed, naming no remaining resume action. It is a **backstop**: it does not depend on the loop having kept the brief current, so a brief untouched since intake is reconciled in full in **one pass**, and the reconciled brief lands **in the same change as the work**. **Reconcile means *to the landed state*, not *mark everything done*** — a todo whose work was genuinely **held out of scope** stays un-completed and rides the follow-up machinery instead; marking it completed would make the doctrine Scanner's `todos-all-done ∧ source-closed` cross-check agree wrongly and clear the brief for retirement, deleting the record of work never done. A brief already matching the landed state is left **unmodified** (the backstop writes what diverges and forces no minimum footprint). Scope is the brief and nothing else: **no** `spec.md` `status`/`approval`, and **no terminal value written into the plan-level `status` dispatch flag** — that enum stays `active | approved` and terminal-ness stays **derived**, matching the Scanner's own never-writes-`status` guard. A mission that **halts** instead of landing is **not** reconciled: a halt is a `pause-mission` checkpoint of the true in-progress state, and reconciling it to "landed" would assert a landing that never happened.
109
113
  - **Hard floors (mandatory stops):** **Clearance** of a narrowing (weakening/deleting an acceptance scenario; pre-authorizable in the CR), **Compatibility** when the semver class exceeds the change-class ceiling (pre-authorizable), and **Conflict** of a logical contradiction in the suite (not pre-authorizable). An obvious stale-mistake contradiction is a conductor-served minor fix; escalate only when both sides are plausibly intended. **Clear the statusline file** on any abort/halt that ends the mission (a hard floor stop, an unconfident-conflict escalation, or any other terminal halt) — the same exit-path clear as handoff and pause; a mid-mission escalation the user resolves in-session (not a halt) is not an exit and leaves the file as-is.
110
114
 
111
115
  ## Suspend and resume