cyber-sdd 0.0.0 → 0.2.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 (71) 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/architect-spec-governance/README.md +1 -0
  10. package/skills/architect-spec-governance/SKILL.md +12 -1
  11. package/skills/blast-estimate/README.md +3 -5
  12. package/skills/blast-estimate/SKILL.md +2 -2
  13. package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
  14. package/skills/builder-impl-governance/SKILL.md +9 -1
  15. package/skills/builder-spec-governance/README.md +1 -0
  16. package/skills/builder-spec-governance/SKILL.md +31 -3
  17. package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
  18. package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
  19. package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
  20. package/skills/check-retired-terms/README.md +18 -0
  21. package/skills/check-retired-terms/SKILL.md +81 -0
  22. package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
  23. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
  25. package/skills/collision-ladder/README.md +3 -5
  26. package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
  27. package/skills/combat-log-governance/SKILL.md +43 -4
  28. package/skills/concept-index/scripts/concept-index.mts +3 -2
  29. package/skills/discover-plans/scripts/discover-plans.mts +5 -2
  30. package/skills/discover-specs/scripts/discover-specs.mts +5 -2
  31. package/skills/doctrine-loop/README.md +6 -0
  32. package/skills/doctrine-loop/SKILL.md +136 -2
  33. package/skills/formation-loop/SKILL.md +21 -1
  34. package/skills/gate-validation-governance/SKILL.md +2 -2
  35. package/skills/impl-producer-governance/SKILL.md +10 -1
  36. package/skills/init/scripts/wire-statusline.mts +5 -2
  37. package/skills/lifecycle-governance/SKILL.md +1 -1
  38. package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
  39. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
  40. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
  41. package/skills/mission-graph/README.md +3 -5
  42. package/skills/mission-graph/SKILL.md +72 -5
  43. package/skills/mission-graph/scripts/mission-graph.mts +505 -16
  44. package/skills/oracle-spec-governance/README.md +7 -2
  45. package/skills/oracle-spec-governance/SKILL.md +21 -4
  46. package/skills/place-node/scripts/place-node.mts +3 -2
  47. package/skills/plan-retirement/README.md +5 -2
  48. package/skills/plan-retirement/SKILL.md +5 -1
  49. package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
  50. package/skills/plugin-contract-governance/SKILL.md +7 -1
  51. package/skills/remediation-governance/SKILL.md +36 -1
  52. package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
  53. package/skills/resolve-tracking/SKILL.md +2 -2
  54. package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
  55. package/skills/sdd/SKILL.md +1 -1
  56. package/skills/spec-format-governance/README.md +1 -1
  57. package/skills/spec-format-governance/SKILL.md +76 -8
  58. package/skills/spec-gate/SKILL.md +18 -2
  59. package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
  60. package/skills/spec-gate/scripts/check-suite.mts +53 -17
  61. package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
  62. package/skills/spec-producer-governance/README.md +1 -1
  63. package/skills/spec-producer-governance/SKILL.md +7 -3
  64. package/skills/ssa-lowering/README.md +3 -5
  65. package/skills/start-mission/README.md +1 -1
  66. package/skills/start-mission/SKILL.md +9 -5
  67. package/skills/suite-format-governance/SKILL.md +43 -4
  68. package/skills/touch-set-correction/README.md +3 -5
  69. package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
  70. package/skills/verify-scenarios/SKILL.md +10 -3
  71. package/skills/verify-scenarios/scripts/verify-scenarios.mts +92 -8
@@ -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
+ }
@@ -4,4 +4,4 @@ Non-user-invocable SDD skill holding the **default spec-producer procedure**: ho
4
4
 
5
5
  Loaded via the harness (`Skill`) by the **conductor** (the main session) when it runs the spec-producer role from the SDD default — the conductor authors **inline** in its own warm context (recorded `produced-by.spec-producer: sdd:automaton`) rather than spawning a producer agent. The grader stays separate: a cold `sdd-spec-judge` reviews the output.
6
6
 
7
- References `sdd:spec-governance` (the universal format bar — including the required `## Use Cases` section and the use-case→scenario coverage rule) plus the resolved oracle + builder + architect actor bars (the spec-gate lens set, forward face) as its self-alignment criteria, and `sdd:ownership-governance` for the write-ownership matrix. Bakes in the grilling discipline (breadth-first, depth one-at-a-time, prose before suite) and the reconcile-toward-the-correct-answer rule for contradictions surfaced during grilling.
7
+ References `sdd:spec-format-governance` (the universal format bar — including the required `## Use Cases` section, its actor-first enumeration, and the rule that scenarios derive from the CFG rather than from the stated use-case prose) plus the resolved oracle + builder + architect actor bars (the spec-gate lens set, forward face) as its self-alignment criteria, and `sdd:ownership-governance` for the write-ownership matrix. Bakes in the grilling discipline (breadth-first, depth one-at-a-time, prose before suite) and the reconcile-toward-the-correct-answer rule for contradictions surfaced during grilling.
@@ -30,9 +30,13 @@ USER_ANSWERS: <answers to previously returned QUESTIONS — or null>
30
30
 
31
31
  2. **Reconcile contradictions toward the correct answer, not the popular one.** When grilling surfaces a conflict — between the `spec.md` body and the `.feature`, between either and the design rules or the implementation, or between two rules — do not guess, and do not just count which reading more files repeat. Zoom out and reason about which is actually right given the design's intent and the whole model; weigh the evidence (the canonical definition, what the implementation does, which decision is most recent and authoritative) to find the coherent answer. Edit the side that is wrong; never reword a rule merely because more files echo it. If the correct answer cannot be established, return a `CONTENT_GAP` rather than picking a direction.
32
32
 
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).
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 per use case its actor / goal, entry point, and extensions, plus the surface-element trace) and the enrichment rules; follow it rather than re-listing sections here (a hardcoded list drifts from the bar).
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
+ **Find the use cases before you name them.** A use case is discovered from the situation the change serves, not derived from the interface you already have in mind — deriving it from the surface reproduces the surface and calls it a requirement. **Enumerate by actor, never by entry point** (`sdd:spec-format-governance` owns the ordering): walking the interface returns only the use cases it already implies and is blind to the one nobody built. **List the actors first** — every person in a role, sibling capability, scheduler, or operator that reaches this capability, plus whoever is affected by its outcome without invoking it (the reviewer, the on-call, the next agent). **Then per actor name the goals** they arrive with — their result, not the call they make; where the answer restates the mechanism ("the caller wants to call it"), you have renamed the function, not found the use case, and the honest move is a `CONTENT_GAP`, never a plausible actor you invented. **Then map goals to entry points**, and report both mismatches: a goal with no entry point is a way in the capability lacks or a goal another node owns, and an entry point serving no listed goal is surface nobody asked for. On `BACKFILL` the source yields only the **served** use cases by construction — recover the unserved ones from the request history, the issue tracker, and recurring workarounds, and record where each came from rather than presenting an inferred set as complete. **Then enumerate the extensions** — walk each use case for **any path from its trigger that does not reach its success outcome**. That criterion decides membership; the recurring kinds (the refusal, the error, the boundary, the partial result, the contended or absent input) are a **prompt to search, not a closed set** (`sdd:spec-format-governance` owns the criterion — a divergence matching none of them still belongs, and a kind that cannot arise is not owed a row). Where nothing can diverge, write `extensions: none — <why>`, so the claim is visible and contestable rather than absent. **Then trace the surface** — take each element the capability exposes (flag, option, parameter, prop, event) and name the use case that needs it and the elements it may not be combined with. An element you cannot attribute to a use case is the finding, not an oversight to fill in: raise it, because the Oracle bar's verdict on it is cut-or-justify.
36
+
37
+ **Then check the extensions against the CFG — the producer-side mirror of the Architect bar.** Where the node carries a `## Control Flow` graph, every extension you just enumerated is a path that graph must actually contain, and every forbidden combination is a decision it must actually refuse. Walk them **both ways**: an edge with no extension is the ordinary uncovered-edge case, and an **extension with no edge** is a divergence the prose claims and the drawn graph cannot take (`sdd:architect-spec-governance` grades exactly this backward). Fix whichever side is wrong — add the missing edge where the extension is real, drop the extension where the graph is right — and never report `STATUS: complete` with a stated extension no edge reaches. Where the graph already reaches every stated extension, **amend neither side**: the check has found nothing, and rewriting a graph or an extension it cleared manufactures churn the Architect lens never asked for. This is a self-alignment duty, not a judge's: settling it here spends no cold round on a contradiction the Architect lens will find every time. On a node with **no** CFG the check is vacuous and fires nothing. 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).
38
+
39
+ 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. **A stated extension earns its scenario by being a path in the CFG**, never by being drawn from the prose — the graph is the single source scenarios derive from, and a suite drawn from a stated list is 1:1 with that list by construction and can no longer surface a hole (`sdd:builder-spec-governance`). So route a divergence covered nowhere back through the graph: add the missing path, and the standing 1:1 edge coverage supplies the scenario. A **forbidden combination** is the same rule in guard form — the CFG carries the decision that refuses it, and the refusal scenario comes from that guard's edge. **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
40
 
37
41
  **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
42
 
@@ -46,7 +50,7 @@ USER_ANSWERS: <answers to previously returned QUESTIONS — or null>
46
50
 
47
51
  **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
52
 
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.
53
+ **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
54
 
51
55
  **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
56
 
@@ -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).
@@ -4,4 +4,4 @@ The single user-facing entry for **changing an SDD project** — triggered by a
4
4
 
5
5
  The session that runs this skill **is the conductor** — the in-session realization of the conductor role; the headless realization is the `automaton` agent. A third realization is **in-session plan-mode preview**: when Claude Code plan mode is active, explore runs its reasoning (classify, seed-intent grill, draft the spec + scenario list, cold spec-judge) but writes no repo files — it renders the drafted spec + suite into the plan file and ends at **ExitPlanMode**, dropping the build-to-learn spikes. On approval the next real explore adopts the preview as the settled draft. Plan mode is detected **in-body**, never via the trigger `description`, so it never re-fires per turn. It supersedes the retired spec-as-mission entries (`create-spec` / `revise-spec`): adding, revising, or deduping part of the project spec is now an **explore-phase operation inside a CR**, not a top-level mission.
6
6
 
7
- Bakes in: step-1 intake (recover the request or fetch an issue URL; scaffold the `.plan.md`); explore as the live grill (classify spec-type + artifact-types, scaffold the node, seed-intent Q&A, the inline spec-producer + cold spec-judge loop with build-to-learn spikes, the iteration cap, the **freeze re-open guard**, observation routing); the internal spec gate (freeze + per-CR gate line to the conductor's own `ledger/` shard + `status: approved`); deliver (spawned impl-producer builder + the internal impl gate); handoff; and the baked autonomy bar (initial strategy, per-gate verdicts, the three hard floors). Pairs with `pause-mission` / `resume-mission`.
7
+ Bakes in: step-1 intake (recover the request or fetch an issue URL; scaffold the `.plan.md`); explore as the live grill (classify spec-type + artifact-types, scaffold the node, actor-first seed-intent Q&A, the inline spec-producer + cold spec-judge loop with build-to-learn spikes, the iteration cap, the **freeze re-open guard**, observation routing); the internal spec gate (freeze + per-CR gate line to the conductor's own `ledger/` shard + `status: approved`); deliver (spawned impl-producer builder + the internal impl gate); handoff; and the baked autonomy bar (initial strategy, per-gate verdicts, the three hard floors). Pairs with `pause-mission` / `resume-mission`.
@@ -39,9 +39,9 @@ For each unit the CR touches:
39
39
  - **Locate or place the node — provisionally.** If a `spec.md` / `README.md` already exists at the target → this is a **revise** (no scaffolding). Otherwise **scaffold** a new node and drop it in a *plausible* home **under the layout the project declared** in its root `spec.md` placement map — `capability-first` groups by what the project *does*, `mirror-source` mirrors the source tree. Placement is judged *within* that declaration, never against a preferred one (`sdd:spec-structure-governance`, "strategy is policy, homes are data"); where no strategy is declared, the `capability-first` default applies. A layered / framework-first **top level** stays discouraged under every strategy (it scatters a capability across folders, breaking node↔folder and degrading scheduling). Consult `project-spec/place-node` (`--concept` → candidate homes; `--name` → "belongs near X" duplicate-catch) and the placement-map routing table (root `spec.md`) for contested overlaps, but **do not agonize**: placement is **provisional** and finalized cheaply at **handoff** (step 4), where a scoped Warden pass relocates it to its blessed home *in the same change* (a pure rename — freeze survives, `sdd:lifecycle-governance`). If the user named no capability, propose a capability folder from the CR and confirm.
40
40
  - **Classify the node** (declared, never inferred): `spec-type: behavioral` (a testable unit → `## Use Cases` + a `<unit>.feature`), `reference` (a shipped non-testable artifact → `## Subject`, no `.feature`), or **descriptive** (an index → no marker). Tag the node's cross-cutting **`concept:`** (the concern it serves — e.g. `lifecycle` / `resolution`; a string or list, orthogonal to `spec-type`; it feeds `project-spec/concept-index`). Also classify each touched file's **artifact-type** (the squad key — resolved per file, **not stored**): **by convention first** (`skill` under `skills/`, `subagent` under `agents/`, …; the extension never decides). On a genuine **ambiguity or a user-flagged path**, consult and record the tiebreaker map `.agents/sdd/artifact-types.toml` and **confirm — never guess** (`sdd:artifact-type` model).
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
- - **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.
42
+ - **Collect seed intent.** For a **new** feature, ask 3–5 targeted questions — **lead with the actors** (who reaches this capability, and who is affected by its outcome without invoking it), then their goals, then the core problem, observable behavior, edge cases / non-goals, and reviewers who must be heard. Ask for the **public interface last, and never first**: an interface offered up front becomes the anchor the use cases get read off, which is the enumeration failure `sdd:spec-format-governance` exists to prevent. 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.3.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.3.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.3.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.3.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
@@ -70,6 +70,45 @@ the current code**, never the baseline to patch. Reading the standing suite and
70
70
  a diff notices is not this procedure — it leaves stale scenarios in place and misses edges the CFG
71
71
  mandates (ADR-0029).
72
72
 
73
+ ## A fold node states its rule in closed form before its scenarios
74
+
75
+ A **fold** (aggregation) node folds several sub-conditions into one verdict — a ready-frontier folding
76
+ reachability against a mutex, a gate-legality aggregate, a per-cell matrix claim. **When the fold
77
+ combines two or more *interacting* sub-conditions, state its rule in closed form — and re-derive that
78
+ rule's soundness against the real data model — before you draw the CFG.** A **single-condition** fold
79
+ may be specified by example; demanding a closed form of it is the failure mode of this rule, not its
80
+ point. Scenarios drawn off a rule never written down are drawn off a rule never *agreed*, and each
81
+ producer-judge round then rewrites a different corner of it: the corpus ran the A/B — the `github-192`
82
+ fence, specified by example, **diverged** (`1 → 1 → 3` contradictions, each manufactured by the prior
83
+ fix, reverted at the cap); `github-224` stated the rule first and **converged** (zero). Closed form
84
+ buys **iteration convergence** — insurance a single-condition fold does not need and a
85
+ multi-condition one rarely survives without. Three qualifications, each a way it is misapplied:
86
+
87
+ - **Fire only on ≥2 *interacting* sub-conditions — over-firing is the failure mode.** A
88
+ single-condition fold by example converges fine (the WAW-mutex touch-set-intersection scenarios
89
+ settled in a handful, no stated rule). Mere aggregation is not the trigger; genuine interaction is.
90
+ - **Closed form is not soundness — re-derive against the real data model.** A rule in closed form,
91
+ even one carrying a proof, can be unsound against the data it folds: `R''` shipped a termination
92
+ proof and still deadlocked, its project-scoped exemption violated by graph-global RAW closure, fixed
93
+ to `R'''` only once the assumption was re-derived against the real graph. A proof over an assumed
94
+ model proves nothing about the real one.
95
+ - **Convergence is not coverage — pair the rule with a mutation sweep and a safety dual.** Stating
96
+ the rule kills the divergence, not the gaps. A **mutation sweep** mutates each interacting condition
97
+ and confirms each break lands on a *distinct* scenario — two mutations breaking one scenario mean
98
+ the conditions collapsed into one and the CFG has fewer real branches than it claims. A **safety
99
+ dual** guards the blind spot a convergence check cannot see: a liveness rule (*some grant path
100
+ exists*, *the frontier advances*) passes an **over-permissive** fold green, because over-permission
101
+ adds paths rather than removing them — pair every liveness scenario with the safety scenario
102
+ asserting the case it structurally cannot observe.
103
+
104
+ **The matrix corollary — a per-cell claim is this rule applied.** An outcome stated per cell of a grid
105
+ of interacting conditions is this rule with the closed form written as the **cell function**. Draw
106
+ **every independent cell as its own CFG branch** and exclude the **degenerate** ones (a cell whose
107
+ outcome reconverges with a sibling collapses under the reconvergence rule above, exactly as a
108
+ **universal** "every cell behaves the same" claim is one convergence scenario, not a grid — the
109
+ `github-278` round-4 draft asserted such a universal one row too wide and dropped it). Confirm the
110
+ cells genuinely independent by the mutation sweep. This is not a separate bar; it is the fold rule.
111
+
73
112
  ## Sections mirror the spec's use-case groups; every scenario binds to a map edge
74
113
 
75
114
  `spec.md` sections the node by **use-case group**, each carrying a drawn **CFG** and an
@@ -119,10 +158,10 @@ only *where the node genuinely owns the routing decision*, and two different dec
119
158
  node's own content, so **the node owns it outright**.
120
159
 
121
160
  The two look alike in shape and differ only in who decides, so **step form does not classify them**
122
- and no mechanical check should try (see `.agents/specs/sdd/ssa-lowering/ssa-lowering.feature`, where
123
- a deletion that read the second case as the first was blocked at the gate and reverted). A
124
- deterministic, fully-owned decision table that selects *what an already-invoked subject does* is
125
- conduct, not engagement — it wants `@behavior`.
161
+ and no mechanical check should try (see the frozen suite of the SDD project spec's `ssa-lowering`
162
+ node, repo-only, where a deletion that read the second case as the first was blocked at the gate and
163
+ reverted). A deterministic, fully-owned decision table that selects *what an already-invoked subject
164
+ does* is conduct, not engagement — it wants `@behavior`.
126
165
 
127
166
  **`@frozen` is the only file-level tag** — it sits on the `Feature`, not a scenario.
128
167
 
@@ -4,11 +4,9 @@ The concrete engine for **touch-set-correction** — a read-only, post-hoc recon
4
4
  Mission's declared touch-set against what its `git diff` actually changed, composing `git diff`,
5
5
  [`resolve-governances`](../resolve-governances/SKILL.md), and `gherkin-cli diff` into the corrected
6
6
  touch-set the mission-graph's single writer records at retirement. Built for the Op2 deferral of the
7
- cyberfleet-batch change request; see
8
- [`.agents/specs/sdd/touch-set-correction/README.md`](../../../../.agents/specs/sdd/touch-set-correction/README.md)
9
- for the authoritative behavior description and
10
- [`touch-set-correction.feature`](../../../../.agents/specs/sdd/touch-set-correction/touch-set-correction.feature)
11
- for the frozen 21-scenario contract.
7
+ cyberfleet-batch change request; the `touch-set-correction` node of the SDD project spec (in the
8
+ cyberplace repository, not shipped in this package) carries the authoritative behavior description
9
+ and the frozen 21-scenario contract.
12
10
 
13
11
  - **Skill contract:** [`SKILL.md`](./SKILL.md)
14
12
  - **Script:** [`scripts/touch-set-correction.mts`](./scripts/touch-set-correction.mts)
@@ -4,8 +4,8 @@
4
4
  // touch-set (recovered from `git diff base..head`). It composes three tools — `git diff` (changed
5
5
  // files), `resolve-governances` (each file's artifact-type, best-effort), and `gherkin-cli diff`
6
6
  // (a touched .feature's changed scenarios) — into one three-way split (confirmed / missed /
7
- // over-declared) plus the corrected touch-set (= the actual touched set). See
8
- // .agents/specs/sdd/touch-set-correction/README.md for the full contract.
7
+ // over-declared) plus the corrected touch-set (= the actual touched set). The touch-set-correction
8
+ // node of the SDD project spec (repo-only) carries the full contract.
9
9
  //
10
10
  // Architecture — pure derivation kept apart from IO, on purpose (mission-graph.mts's convention):
11
11
  // - isFeature / fileToNode / reconcile / assembleCorrection are PURE: they take and return plain
@@ -27,9 +27,9 @@
27
27
  // Pure functions are exported for node:test; running the file directly drives the CLI.
28
28
 
29
29
  import { execFileSync } from 'node:child_process'
30
- import { readFileSync } from 'node:fs'
30
+ import { readFileSync, realpathSync } from 'node:fs'
31
31
  import { dirname, join, relative, resolve, sep } from 'node:path'
32
- import { fileURLToPath } from 'node:url'
32
+ import { fileURLToPath, pathToFileURL } from 'node:url'
33
33
  import { type DiffReader, diffFeatures } from 'gherkin-cli'
34
34
 
35
35
  // ── Types ──
@@ -415,4 +415,6 @@ export function main(argv: string[]): number {
415
415
  return 0
416
416
  }
417
417
 
418
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
418
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
419
+ process.exit(main(process.argv.slice(2)))
420
+ }
@@ -29,6 +29,13 @@ artifact-type already falls through to SDD defaults).
29
29
  - **Many-to-one is fine.** Two tests can bind the same key; the fold is PASS only if none of them
30
30
  fail. A test that maps to an already-covered key and isn't the canonical rename shows up as an
31
31
  EXTRA (diagnostic, not a failure) — leave it.
32
+ - **A punctuation-only near-miss still binds.** When no result matches a key exactly, the fold
33
+ retries on a comparison key that folds curly quotes/apostrophes, dashes, and the ellipsis to their
34
+ ASCII forms and collapses whitespace — so a pasted `’` for a `'` does not land the same scenario in
35
+ BOTH the UNBOUND list and the EXTRA list with nothing linking them. The bind is reported as a
36
+ **PROBABLE TITLE MISMATCH** naming both verbatim titles, so the typo still gets fixed. Nothing
37
+ rewrites a title, an **exact** match always wins, case is **not** folded, and an **ambiguous** fold
38
+ (two candidates folding alike) stays UNBOUND rather than binding the wrong one.
32
39
 
33
40
  ## Config schema
34
41
 
@@ -76,9 +83,9 @@ node "<skill>/scripts/verify-scenarios.mts" \
76
83
  - `--report <xml>` bypasses `--config` entirely — a single ad-hoc junit source, no command.
77
84
  - `--run` executes each source's `command` first; without it, existing reports are read as-is.
78
85
  - Default output is a readable per-scenario table + a `N/M BOUND, P pass, F fail, U unbound`
79
- summary line + any EXTRA keys. `--format json` emits
80
- `{node,total,bound,pass,fail,unbound,scenarios[],extras[]}`. `--format toon` emits the repo's
81
- TOON tabular form.
86
+ summary line + any EXTRA keys + any PROBABLE TITLE MISMATCH pairs. `--format json` emits
87
+ `{node,total,bound,pass,fail,unbound,scenarios[],extras[],mismatches[]}`. `--format toon` emits the
88
+ repo's TOON tabular form.
82
89
  - Exit code is non-zero when any scenario is UNBOUND or FAIL; zero only at full BOUND+PASS.
83
90
 
84
91
  ## Monorepo rooting — `--feature-root` vs. `--root`