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
@@ -9,8 +9,9 @@
9
9
  // reaches the output. No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions
10
10
  // are exported for node:test; running the file directly drives the CLI.
11
11
 
12
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
12
+ import { existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
13
13
  import { join } from 'node:path'
14
+ import { pathToFileURL } from 'node:url'
14
15
 
15
16
  export const BEGIN_MARKER = '<!-- BEGIN generated: by-concept (project-spec/concept-index) -->'
16
17
  export const END_MARKER = '<!-- END generated: by-concept -->'
@@ -240,6 +241,6 @@ export function main(argv: string[]): number {
240
241
  return 0
241
242
  }
242
243
 
243
- if (import.meta.url === `file://${process.argv[1]}`) {
244
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
244
245
  process.exit(main(process.argv.slice(2)))
245
246
  }
@@ -18,8 +18,9 @@
18
18
  // No dependencies (the repo's node-≥23.6 / no-deps convention). --format json for a flat
19
19
  // array; default output is TOON (the token-efficient tabular form the gateway scans).
20
20
 
21
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
21
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
22
22
  import { join } from 'node:path'
23
+ import { pathToFileURL } from 'node:url'
23
24
 
24
25
  export const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed'])
25
26
 
@@ -209,4 +210,6 @@ export function main(argv: string[]): number {
209
210
  return 0
210
211
  }
211
212
 
212
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
213
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
214
+ process.exit(main(process.argv.slice(2)))
215
+ }
@@ -26,8 +26,9 @@
26
26
  // default output is TOON (the token-efficient tabular form the gateway scans). --resolve <name>
27
27
  // filters to the exact name matches (0 rows = none, 1 = resolved, >1 = ambiguous).
28
28
 
29
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
29
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
30
30
  import { join } from 'node:path'
31
+ import { pathToFileURL } from 'node:url'
31
32
 
32
33
  export const LIFECYCLE_STATUSES = new Set(['draft', 'approved', 'implemented', 'deprecated'])
33
34
 
@@ -393,4 +394,6 @@ export function main(argv: string[]): number {
393
394
  return 0
394
395
  }
395
396
 
396
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
397
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
398
+ process.exit(main(process.argv.slice(2)))
399
+ }
@@ -12,4 +12,10 @@ the one project `ledger/` directory. The entry **shape** and the matchable `caus
12
12
  `sdd:combat-log-governance` (deferred, never restated). The Council holds keep-or-cut; the `sdd`
13
13
  gateway surfaces the count of pending unratified strategy when the Council re-enters.
14
14
 
15
+ Separately, during its pass, the Scanner also cross-checks each plan brief's `todos-all-done`
16
+ against its `source-closed` to **derive the retirement clearance set** — it never autofixes a
17
+ plan's `status`; agreement feeds `sdd:plan-retirement`'s existing `--retire` input, disagreement
18
+ surfaces a flagged finding in the Scanner's pass summary. This is distinct from strategy-drafting
19
+ above: a flagged finding is never a ledger write.
20
+
15
21
  Plan retirement (doctrine's last retro step) is the sibling `sdd:plan-retirement` skill.
@@ -46,6 +46,40 @@ Each is an entry-point: a lifecycle-grained trigger, its post-hoc input, and the
46
46
 
47
47
  The Scanner **observes** the terminal transitions; it never writes a mission's `status`.
48
48
 
49
+ ## Idempotent Ship/Kill: detect an already-distilled mission by parsing the ledger, never grepping
50
+
51
+ The **Ship** (`→ implemented`) and **Kill** (`→ deprecated`) triggers can meet the **same** terminal
52
+ transition more than once — a later pass, a resumed segment, a fresh re-invocation. So they are
53
+ **idempotent**: **before drafting** Ship/Kill strategy for a mission, first detect whether that
54
+ mission was **already distilled** — a prior `strategy` entry whose **`distills` field equals the
55
+ mission's `<cr-ref>`** — and if so, **draft nothing** (no duplicate strategy for a mission already
56
+ recorded as distilled).
57
+
58
+ **Determine distilled-ness by a structured JSONL parse, never a substring or regex text match.**
59
+ The ledger shards are JSONL (one JSON object per line). **Reuse the existing parse contract** rather
60
+ than hand-rolling one: run the exported **`distilledCrRefs`** engine from
61
+ `../plan-retirement/scripts/retire-plans.mts` over the project `ledger/` directory and membership-test
62
+ the mission's `<cr-ref>` against the set it returns, e.g.
63
+
64
+ ```
65
+ node -e 'import("../plan-retirement/scripts/retire-plans.mts").then(m => console.log([...m.distilledCrRefs(process.argv[1])].join("\n")))' <ledger-dir>
66
+ ```
67
+
68
+ `distilledCrRefs` parses **each line** with `JSON.parse` (keying only on `kind === "strategy"` with a
69
+ non-empty `distills`), so it is correct against **any** valid-JSON formatting the Scanner or another
70
+ writer emits — a **space after the colon** in a pretty-printed entry (`"distills": "<cr-ref>"` vs
71
+ `"distills":"<cr-ref>"`), a line-wrapped or reordered object — where a naive no-space substring grep
72
+ (`"distills":"<cr-ref>"`) **silently under-counts**, reports an already-distilled mission as
73
+ undistilled, and **re-drafts** it. This is the same defect class as *grep is blind to wrapped terms*;
74
+ a distilled-detection that under-counts causes false "undistilled" conclusions and wasted or duplicate
75
+ drafting.
76
+
77
+ Reusing the engine also inherits its exact semantics: it keys on the **structured `distills` field
78
+ only** (an `<cr-ref>` appearing merely inside another entry's `evidence` cross-references does **not**
79
+ count as distilled), **tolerates malformed and blank lines** (they are skipped, not fatal), and counts
80
+ an **unratified** distilling entry — the Scanner's default — the same as a ratified one (the question
81
+ is what was distilled, not sign-off). This is a **read** over the ledger; it changes nothing you write.
82
+
49
83
  ## Inputs: combat log (contract) vs transcripts (enrichment)
50
84
 
51
85
  The Scanner reads **persisted artifacts post-hoc** — never live subagent context.
@@ -64,6 +98,81 @@ post-merge loop keeps the dimension; the **numeric** breakdown lives only in tra
64
98
  never under it without a request). No raw token-cost number is written to the committed log — only
65
99
  the categorical class (the safe-to-publish floor, `sdd:combat-log-governance`).
66
100
 
101
+ ## Validate before drafting — a plan or log is a hypothesis, not present truth
102
+
103
+ A persisted plan, combat log, or ledger line is **history**, never authoritative present truth. Every
104
+ candidate improvement it surfaces — a gap to build, a defect to fix — is a **hypothesis about the
105
+ current codebase** (the repo's own principle: *a source-read is a hypothesis, refuted by a repro
106
+ against current code*). **Before you draft**, run a validation gate on each candidate: read the
107
+ CURRENT code and decide whether the flagged gap still exists, or has since been **built / fixed /
108
+ superseded** (by a later mission, a landed PR, a governance change).
109
+
110
+ - **Resolved → cut.** A candidate current code already resolves is **cut**: draft **no** build-or-fix
111
+ strategy and emit **no** issue. Record the cut **explicitly, never silently** — append a `strategy`
112
+ entry marked **`disposition: resolved`** to your own shard, carrying the **resolving current-code
113
+ evidence** (the repro that refuted the hypothesis) in `evidence`. A `disposition: resolved` line is
114
+ a **tombstone**: not an actionable recommendation, so it emits no issue and is **not counted toward
115
+ pending strategy** at the gateway. It exists so the cut is auditable and a later run does not
116
+ silently re-surface the same closed candidate.
117
+ - **Still open → draft.** A candidate current code does **not** resolve is a real improvement: draft
118
+ `strategy` for it marked **`disposition: open`** (the default; a line without the field grandfathers
119
+ as open), which counts toward pending strategy, and emit its issue (below).
120
+
121
+ The gate applies to any candidate that asserts an **unmet gap or defect**, whether surfaced by a
122
+ **plan or a combat log** (symmetric — each source has both a resolved→cut and a still-open→draft
123
+ path). A pure distilled **retro lesson** (a success pattern, not a gap claim) carries no gap to
124
+ validate and is drafted without a current-code check. Set the disposition **once at write** — never
125
+ flip it (append-only); the Council's later keep-or-cut on a `disposition: open` line is a separate act
126
+ (the keep-or-cut plan), not a field edit. The `disposition` field's shape is owned by
127
+ `sdd:combat-log-governance`.
128
+
129
+ This gate is what stops the loop reinforcing a **stale cache** — drafting "build X" for an X a later
130
+ mission already built.
131
+
132
+ ## Cold-instrument evidence — non-author measurement + ablation before a rule-level recommendation
133
+
134
+ A self-authored measurement is untrustworthy exactly when it grounds a **rule-level decision**: the
135
+ author's own blind spot is baked into the instrument meant to catch it (op6/#224 — a masked generator
136
+ "proved" its own conclusion twice, each ratified then walked back; op6-m5/#254 — a proposed revival
137
+ measured Δ=0 dead weight, caught before landing). Two rules extend the validate-before-drafting gate to
138
+ your rule-level recommendations. This is **the canonical statement** of the non-author-evidence
139
+ standard — other faces (`sdd:spec-producer-governance`'s dimension/cut grounding) reference it.
140
+
141
+ - **Non-author evidence.** Draft a recommendation to **adopt a rule, drop a rule, or set a threshold**
142
+ only when its grounding measurement was **produced by a party other than the proposer, *or*
143
+ independently reviewed by such a party, *or* ablated against a freshly and adversarially constructed
144
+ case**. A measurement on the proposer's **own generator or harness alone** is **withheld**: draft no
145
+ recommendation on it, and record that it needs non-author or fresh-adversarial evidence. A measurement
146
+ grounding **no** rule-level decision is unconstrained by this rule.
147
+ - **Ablation before landing a revival.** Draft a candidate **reviving a dead measurement dimension** to
148
+ land only when an ablation against a control shows the dimension is **loseable**; **state the revived
149
+ rule abstractly with no worked example** (lifting a probe scenario's apparatus into the rule is
150
+ **absorption** — the probe then grades nothing). A revival whose ablation shows **no difference** from
151
+ the control is **dead weight**: **cut** it (a `disposition: resolved` tombstone, as above) and draft
152
+ no land recommendation.
153
+
154
+ ## Improvement output — validated-open findings become tracked issues
155
+
156
+ The loop's **actionable output** for a validated-open improvement is a **new tracked issue**
157
+ (`gh issue create`) — one titled, bodied issue per real improvement, **cross-linking the evidence**
158
+ that drove it. This grounds the improvement plan in current code, not the stale narrative of a retired
159
+ plan. The ledger `strategy` line stays the **provenance**; the issue is what a later mission is started
160
+ from.
161
+
162
+ - **Dedupe first.** Before filing, dedupe against the forge's existing issues — **open and closed**
163
+ (at least two keyword combinations: the full title, then the core noun/verb) — and on a mixed set
164
+ file only the unmatched.
165
+ - **Emit is not dispatch.** Emitting an issue leaves the `strategy` **unratified** and spawns **no**
166
+ mission — it opens no CR and admits nothing to the mission graph (that is the graph's single writer's
167
+ act). Keep-or-cut stays the Council's; the issue re-enters SDD only when a **later** mission is
168
+ started from it.
169
+ - **Outward-publish floor.** Compose the issue body to the same outward-publish floor the handoff
170
+ follow-up issues meet (owned by the handoff unit — do not restate it): **self-contained** (a reader
171
+ who cannot see the mission's internal artifacts can act on it), **no production-internal artifact
172
+ reference** (no ledger shard filename, no combat-log or plan-brief path), plus everything the
173
+ committed-record floor bans (absolute paths, `$HOME`/`$USER`, usernames, secrets, raw numbers).
174
+ Carry an **agent-filed marker** and name the evidence it was distilled from.
175
+
67
176
  ## Where strategy lands
68
177
 
69
178
  Every `strategy` entry lands in the **one project ledger** — the `ledger/` directory sibling of the
@@ -72,7 +181,9 @@ random hex once per session), so two concurrent Scanner runs write distinct shar
72
181
  There is no per-spec log to route to under the project-spec model. The Scanner's `handle` is
73
182
  `sdd-scanner`. Every entry is **unratified** (`ratified: false`) and carries its **driving evidence**
74
183
  (the distilled `cause` recurrence that drove it), per the shape in `sdd:combat-log-governance`. The
75
- shard is append-only — the next `seq` within it, never an edit; ledger lines carry **no `ts`**.
184
+ shard is append-only — the next `seq` within it, never an edit; ledger lines carry **no `ts`**. Every
185
+ entry also carries its validation **`disposition`** (`open` for a drafted still-open improvement,
186
+ `resolved` for a validation tombstone; see *Validate before drafting* above) — set once at write.
76
187
 
77
188
  **Record the distilled subject.** When the entry is drafted from a **Ship** (`→ implemented`) or
78
189
  **Kill** (`→ deprecated`), set `distills: <cr-ref>` to the **one mission it was distilled from** —
@@ -90,8 +201,31 @@ strategy re-enters as a **new CR** that re-tunes the doctrine and grows the corp
90
201
  **PRUNE** removes the stale convention). On **cut**, it stays unratified and absent from the
91
202
  corpus.
92
203
 
204
+ ## Stale plan frontmatter — deriving the retirement clearance set, never autofixing status
205
+
206
+ A plan brief's frontmatter `status` can lag reality (its own todos finish, or its source issue
207
+ closes, without the value being flipped) but there is **no legal terminal value** to autofix it
208
+ into — the plan-level `status` is a two-value dispatch flag (`active | approved`), and the
209
+ contract's own answer to "this mission is over" is **retirement**, not a status write
210
+ (`sdd:combat-log-governance`'s home, `design/provenance-model.md`). So during its pass, for each
211
+ brief under `.agents/plans/`, the Scanner cross-checks `todos-all-done` (every `todos[].status`
212
+ completed) against `source-closed` (the declared `source` queried natively, the same way
213
+ `plan-retirement`'s own clearance check queries it) — never writing `status` either way:
214
+
215
+ - **Both agree terminal** → the cr-ref is included in the **retirement clearance set** the Scanner
216
+ passes as `plan-retirement`'s existing `--retire` input. No new deletion mechanism —
217
+ `plan-retirement` still runs its own gated sweep.
218
+ - **Both agree non-terminal** → no clearance, no finding.
219
+ - **They disagree** → excluded from the clearance set; the Scanner surfaces a flagged finding
220
+ naming the disagreement in its pass summary (its only channel — it returns only its final
221
+ message), for a human to resolve. A flagged finding is ephemeral and never a ledger write — it
222
+ is neither a `kind: strategy` entry nor a `kind: report` line, and it is distinct from the
223
+ validated-open-improvement finding above that becomes a tracked issue.
224
+
93
225
  ## Plan retirement
94
226
 
95
227
  Doctrine's **last retro step** — the gated, idempotent **tracked deletion** of a retired plan — is
96
228
  a separate unit (`sdd:plan-retirement`). The distill (writing `strategy` here) fires early, at
97
- `→ implemented`; the delete is a later step gated on source = `done`/merged **and** distilled.
229
+ `→ implemented`; the delete is a later step gated on source = `done`/merged **and** distilled. The
230
+ source-cleared set `plan-retirement` consumes via `--retire` is, per above, the Scanner's derived
231
+ retirement clearance set — never a raw, uncross-checked source-only judgment.
@@ -67,7 +67,7 @@ structure and discovery (`corpus/` + `project-spec/`), not on any given station
67
67
  | Act | Trigger | Station (`corpus/` + `project-spec/`) | Output |
68
68
  |---|---|---|---|
69
69
  | **Audit node-shape** | a formation pass fires post-mission | `check-spec-structure` | a finding set: untagged-node (blocking) + oversized-node (advisory), each naming the node |
70
- | **Split an oversized node** | the Warden's `@rubric` breadth-vs-depth judgment routes an oversized-node's shape profile to breadth-overflow | `check-spec-structure` | a sub-node split; depth-overflow instead down-levels via the scenario→test bridge (`verify-scenarios`) or is redesigned — the engine emits only the profile, never the route |
70
+ | **Split an oversized node** | the Warden's `@rubric` breadth-vs-depth judgment routes an oversized-node's shape profile to breadth-overflow | `check-spec-structure` | a sub-node split whose **organizing axis is first checked against a real capability/command boundary** (below); depth-overflow instead down-levels via the scenario→test bridge (`verify-scenarios`) or is redesigned — the engine emits only the profile, never the route |
71
71
  | **Reconcile drift / contradiction** | prose↔suite drift, or two nodes contradict | `align-spec` | a reconcile finding (drift fixed by direction; contradiction → align the losing side) |
72
72
  | **Dedupe cross-node scenario overlap** | the same behavior is specified in two nodes' suites — a hard collision the scenario rung cannot see (spec-level SSA) | `check-scenario-overlap` | a dedup finding naming both nodes; the Warden's `@rubric` arm confirms real overlap and **assigns a single owning node** (one behavior = one scenario in one node) |
73
73
 
@@ -76,6 +76,26 @@ raises **no** untagged finding; nodes (or governances) that **agree** raise **no
76
76
  two nodes that specify **no** shared behavior raise **no** dedup finding. The acts are evidence-gated,
77
77
  not run unconditionally.
78
78
 
79
+ **The split-axis check — validate the organizing axis, not just the granularity.** An oversized node
80
+ is a **granularity** signal, but a split carves it along a proposed **organizing axis**, and the
81
+ wrong axis produces a split CR that gets **superseded rather than landed** — a full formation cycle
82
+ wasted. So before proposing a split (self-clearing it or escalating its CR), the Warden **checks the
83
+ proposed axis against a real capability/command boundary**: each side must map to a **distinct
84
+ capability, command, or lifecycle phase**, not merely to an **internal implementation grouping** that
85
+ shares one boundary.
86
+
87
+ - an axis where each side is a distinct capability/command boundary **passes** — the split is
88
+ proposed on that axis;
89
+ - an axis that only regroups internal implementation under one shared boundary **fails** — the Warden
90
+ does **not** carve sub-nodes on it, and raises the oversize as a **wrong-axis reorganization** (the
91
+ axis is wrong), not a granularity split to carve as-proposed.
92
+
93
+ An oversize can be a symptom of the **wrong axis**, not just wrong granularity: turn "this node is too
94
+ big" into "too big **along which axis** — and is that axis real?" (Precedent: a killed
95
+ `identity/`→`presence/` split proposed a plausible-but-unreal axis and was superseded by a realignment
96
+ that split along the package's actual command boundary; the producer/consumer boundary had even been
97
+ validated as sound, yet the *split axis* was never checked against a real command boundary.)
98
+
79
99
  Alongside its findings a pass surfaces an **advisory layout-quality signal** — the scheduler's
80
100
  **false-conflict rate** doubles as a **partition-quality metric**: a layout that keeps node↔folder
81
101
  clean holds the rate low, and one that scatters a single concern across folders drives it up. Read
@@ -25,7 +25,7 @@ rules by reading frontmatter. The `(status, markers, .feature, approval)` tuple
25
25
  - an `approval.<gate>` is `verdict: pause` carrying a `by` — a pause is always the agent's act and omits `by`.
26
26
  - an `approval.<gate>` is `verdict: approve` with no `by` — an approve must record its approver.
27
27
  - an `approval.<gate>` is `by: agent` with no `why` block — a self-assertion must record its derivation.
28
- - an `approval.<gate>` has a `cause` other than `dimension` or `ceiling` — the stop cause is a closed enum.
28
+ - an `approval.<gate>` has an off-enum `cause` (not `dimension` / `clearance` / `ceiling`) **without** a `cause-candidate: true` flag — the stop cause is a closed enum grown only by ratification, so an unflagged off-enum value fails closed; a `cause-candidate: true`-flagged off-enum value is **legal** (the off-enum candidate discipline, `sdd:combat-log-governance`).
29
29
  - an `approval.<gate>` is `verdict: pause` on a gate the spec has already passed (spec once `approved`/`implemented`, impl once `implemented`).
30
30
  - `status: approved` or `implemented` with no `approval.spec` `verdict: approve` + `by` — the spec gate has no recorded ratification.
31
31
  - `status: implemented` with no `approval.impl` `verdict: approve` + `by` — the impl gate has no recorded ratification.
@@ -83,5 +83,5 @@ A required production role **always** resolves to a real producer — a plugin a
83
83
  for that role. When a gate runs and a required role has **no resolvable producer** (not a plugin
84
84
  agent and not even an SDD default), the gate **fails closed** with a blocker; it advances nothing.
85
85
  This is a **structural** error, the same fail-closed class as a malformed `produced-by` entry or an
86
- off-enum `cause` (defined in `sdd:combat-log-governance`). Distinct from availability: a recorded
86
+ **unflagged** off-enum `cause` (a `cause-candidate: true`-flagged one is legal; `sdd:combat-log-governance`). Distinct from availability: a recorded
87
87
  producer whose plugin is merely uninstalled is **flagged**, not blocked.
@@ -47,6 +47,14 @@ MODE: explore | implement
47
47
  the inner rules — the pyramid's base, separate from the per-scenario duty. A non-deterministic
48
48
  subject has no such layer — verify at the acceptance level only.
49
49
 
50
+ **Instrument subject → mutation-sweep-first (the cold-instrument doctrine).** When the **subject is
51
+ itself a measurement or verification instrument** — a fixture, mutation set, ablation generator,
52
+ falsifier, judge, or check — a **mutation sweep is the default verification method** and reading the
53
+ instrument is **supplementary**, not the primary check. This **overrides**, for an instrument subject
54
+ only, the verify-as-high default above; a non-instrument subject keeps that default unchanged.
55
+ Reading an instrument finds a defect or two where a sweep finds many, and a "cannot-fail" defect that
56
+ survives every read dies to the first mutation the instrument fails to catch.
57
+
50
58
  5. **Never modify `spec.md` or the suite** — four-eyes. A behavior-changing gap is a
51
59
  `CONTENT_GAP` / `BLOCKER`, never an in-place edit. Never change or remove a `@pinned` scenario —
52
60
  propose it and surface for user authorization (`sdd:ownership-governance`).
@@ -80,6 +88,7 @@ OBSERVATIONS: [ { owner: architect | strategist, note, evidence } ]
80
88
  3. **Author one check per frozen scenario** anchored to it; prefer running the scenario directly so
81
89
  the oracle stays spec-owned; an unverifiable scenario is a reported gap.
82
90
  4. **Verify as high as it doesn't hurt** — record level + why; deterministic combinatorics go to unit
83
- tests (the pyramid base).
91
+ tests (the pyramid base). **An instrument subject** (fixture / mutation set / ablation generator /
92
+ falsifier / judge / check) inverts the default: **mutation-sweep-first**, reading supplementary.
84
93
  5. **Never modify `spec.md` / the suite; never touch a `@pinned` scenario without user
85
94
  authorization.**
@@ -25,9 +25,10 @@
25
25
  // fresh wire ever adopts it.
26
26
  // Pure functions are exported for node:test; running the file directly drives the CLI. No deps.
27
27
 
28
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
28
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
29
29
  import { homedir } from 'node:os'
30
30
  import { dirname, join } from 'node:path'
31
+ import { pathToFileURL } from 'node:url'
31
32
 
32
33
  export const STATUS_FILE = '.agents/sdd/statusline'
33
34
  export const SETTINGS_FILE = '.claude/settings.json'
@@ -273,4 +274,6 @@ export function main(argv: string[]): number {
273
274
  return 0
274
275
  }
275
276
 
276
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
277
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
278
+ process.exit(main(process.argv.slice(2)))
279
+ }
@@ -24,7 +24,7 @@ approval: # per-gate verdict
24
24
  spec: # verdict: approve | pause | reject
25
25
  verdict: approve
26
26
  by: agent # agent (self-asserted, provisional) | <human name> (ratified); omitted on pause
27
- cause: dimension # dimension | ceiling — what drove the verdict
27
+ cause: dimension # dimension | clearance | ceiling — what drove the verdict (off-enum → flag cause-candidate: true)
28
28
  why: # verdict derivation (agent self-assertion or pause)
29
29
  floor: <none | clearance | conflict | compatibility | consent>
30
30
  blast: <low|high — reason>
@@ -17,8 +17,9 @@
17
17
  // Pure functions are exported for node:test; running the file directly drives the CLI. No deps
18
18
  // (the repo's node-≥23.6 convention).
19
19
 
20
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
20
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
21
21
  import { dirname, join } from 'node:path'
22
+ import { pathToFileURL } from 'node:url'
22
23
 
23
24
  const IGNORE_FILE = '.agents/sdd/.sddignore'
24
25
  const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
@@ -291,4 +292,6 @@ export function main(argv: string[]): number {
291
292
  return 0
292
293
  }
293
294
 
294
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
295
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
296
+ process.exit(main(process.argv.slice(2)))
297
+ }
@@ -22,8 +22,9 @@
22
22
  // Pure functions are exported for node:test; running the file directly drives the CLI. No deps (the
23
23
  // repo's node-≥23.6 convention).
24
24
 
25
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
25
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
26
26
  import { dirname, join } from 'node:path'
27
+ import { pathToFileURL } from 'node:url'
27
28
  import { parseSourcesToml, type SourceConfig } from '../../verify-scenarios/scripts/verify-scenarios.mts'
28
29
 
29
30
  export type { SourceConfig }
@@ -153,4 +154,6 @@ export function main(argv: string[]): number {
153
154
  return 0
154
155
  }
155
156
 
156
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
157
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
158
+ process.exit(main(process.argv.slice(2)))
159
+ }
@@ -17,8 +17,9 @@
17
17
  // are implicit and cannot be curated here.
18
18
  // Pure functions are exported for node:test; running the file directly drives the CLI. No deps.
19
19
 
20
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
20
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
21
21
  import { dirname, join } from 'node:path'
22
+ import { pathToFileURL } from 'node:url'
22
23
 
23
24
  const ANCHORS_CONFIG = '.agents/sdd/spec-anchors.toml'
24
25
  const LIFECYCLE_STATUSES = new Set(['draft', 'approved', 'implemented', 'deprecated'])
@@ -325,4 +326,6 @@ export function main(argv: string[]): number {
325
326
  return 0
326
327
  }
327
328
 
328
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
329
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
330
+ process.exit(main(process.argv.slice(2)))
331
+ }
@@ -3,11 +3,9 @@
3
3
  The concrete engine for the **mission-graph kernel** — a git-tracked, append-only work-graph store
4
4
  (nodes, edges, status, tombstones, schema `v:1`) folded into a read-only `ready` frontier and a
5
5
  `cycles` repair view, plus an Operation deliverability check. Built for Op1.M1 of the
6
- **cyberfleet-batch** change request; see
7
- [`.agents/specs/sdd/mission-graph/README.md`](../../../../.agents/specs/sdd/mission-graph/README.md)
8
- for the authoritative behavior description and
9
- [`mission-graph.feature`](../../../../.agents/specs/sdd/mission-graph/mission-graph.feature) for
10
- the frozen 36-scenario contract.
6
+ **cyberfleet-batch** change request; the `mission-graph` node of the SDD project spec (in the
7
+ cyberplace repository, not shipped in this package) carries the authoritative behavior description
8
+ and the frozen 36-scenario contract.
11
9
 
12
10
  - **Skill contract:** [`SKILL.md`](./SKILL.md)
13
11
  - **Script:** [`scripts/mission-graph.mts`](./scripts/mission-graph.mts)
@@ -25,8 +25,9 @@ plus a zero-dependency **fold** into two read-only views and one write-time guar
25
25
 
26
26
  It carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention). Pure
27
27
  derivations (`fold`, `ready`, `cycles`, `checkOperation`, `proposeEdge`) take and return plain data
28
- only — no fs access — kept apart from a thin store-IO seam (v1: an in-tree JSONL file) so a later
29
- swap to a shared, branch-independent store never touches them.
28
+ only — no fs access — kept apart from a thin store-IO seam with **two backends**: an in-tree JSONL
29
+ file and the branch-independent orphan ref `refs/sdd/mission-graph`, resolved at run time. The swap
30
+ never touches a derivation.
30
31
 
31
32
  ## Run it
32
33
 
@@ -45,11 +46,77 @@ node "<skill>/scripts/mission-graph.mts" append edge --kind RAW|parent-child|dis
45
46
 
46
47
  node "<skill>/scripts/mission-graph.mts" append tombstone --target node --id <id>
47
48
  node "<skill>/scripts/mission-graph.mts" append tombstone --target edge --kind <kind> --from <id> --to <id>
49
+
50
+ # store home + reach (idempotent; --root defaults to .)
51
+ node "<skill>/scripts/mission-graph.mts" migrate [--root .]
52
+ node "<skill>/scripts/mission-graph.mts" sync [--root .] [--remote origin]
48
53
  ```
49
54
 
50
- - Default `--root` is the current directory; the store lives at
51
- `<root>/.agents/mission-graph/events.jsonl`. Default `--format` is **TOON**; `--format json`
52
- emits the same records as JSON for non-LLM consumers.
55
+ - Default `--root` is the current directory. The store home is resolved at run time — the orphan ref
56
+ `refs/sdd/mission-graph` inside a git work-tree, else the in-tree file
57
+ `<root>/.agents/mission-graph/events.jsonl`; `MISSION_GRAPH_STORE=in-tree|orphan-ref` overrides.
58
+ Default `--format` is **TOON**; `--format json` emits the same records as JSON for non-LLM
59
+ consumers.
60
+ - **`migrate` retires the in-tree seed** after copying it into the ref — `git rm` the store file,
61
+ leaving the deletion **staged, not committed** (the engine's only working-tree write; it reports that
62
+ the commit is required for the migration to reach other clones). **Why it is not optional:** the seed
63
+ is a *tracked* file, so a seed left behind travels to every clone, where `resolveBackend` prefers it
64
+ over the (never-fetched) ref — the clone reads a **stale list that looks valid** and no `sync` can
65
+ dislodge it, because the seed is precisely what stops the clone consulting the ref. Two guards:
66
+ it **refuses** to retire a seed whose every line is not already in the ref (deleting otherwise would
67
+ be the data loss migration exists to avoid), and it **does** retire a seed an earlier migration left
68
+ behind, so a pre-fix project is repaired by re-running `migrate`.
69
+ - **Reach.** `refs/sdd/*` is outside `refs/heads/*`, so git's default refspec never fetches or
70
+ pushes it: the ref is shared by every **worktree of one clone** and travels no further on its own.
71
+ `sync` is the one step that widens that reach.
72
+ - **`sync` names the ref explicitly on its own command line**, so it needs **no git configuration and
73
+ writes none** — not a fetch refspec, not a push refspec. Verified: a fresh clone carrying only the
74
+ default `+refs/heads/*:refs/remotes/origin/*` fetches the store ref fine when the refspec is given
75
+ as an argument. The engine never touches `.git/config`.
76
+ - **No refspec is ever written in its forced (`+`-prefixed) form.** A leading `+` means *force*: with
77
+ `+refs/sdd/*:refs/sdd/*` in play, a diverged store ref is overwritten at **exit 0** with
78
+ `(forced update)`, silently destroying the local appends — the exact data loss the compare-and-swap
79
+ write path exists to prevent. Non-forced, the same transfer reports
80
+ `! [rejected] (non-fast-forward)` and **exits 1**, leaving the ref intact.
81
+ - `sync` is **fast-forward only**, in both directions. Its cases are a **total partition** of
82
+ `(local ref, remote ref)` — each side present or absent, and when both are present the pair is
83
+ exactly one of identical / one an ancestor of the other (either way) / neither:
84
+
85
+ Two checks run **before** the partition, in this order — (1) **backend**: under the in-tree backend,
86
+ no-op reporting the home, exit 0, no network touched (an in-tree project must not fail merely for
87
+ being offline); (2) **reachability**: a remote unreachable *or not configured* fails non-zero and is
88
+ **never** reported as "remote has none". Then:
89
+
90
+ | local | remote | action |
91
+ |---|---|---|
92
+ | absent | absent | report **both absent**, change nothing |
93
+ | present | absent | push |
94
+ | absent | present | collect |
95
+ | present | present, same commit | report agreement, change nothing |
96
+ | present | present, local is ancestor | fast-forward local |
97
+ | present | present, remote is ancestor | push |
98
+ | present | present, neither is ancestor | **refuse loudly**, report both tips, change nothing |
99
+
100
+ There is no `--force`; divergence means two write-deciders, which the store surfaces rather than
101
+ resolves. **The reachability row is load-bearing:** `git ls-remote` fails the same way for an
102
+ unreachable remote as for a missing ref, so a naive read conflates them and lands on "both absent" —
103
+ reporting a benign empty state for a remote it never saw. That is the silent-empty defect this whole
104
+ capability exists to remove; establish reachability first and fail non-zero when it cannot be
105
+ established.
106
+ - **Making an ordinary `git fetch` collect the ref too is out of scope here.** That means writing a
107
+ persistent fetch refspec into a clone's config — an opt-in, per-clone setup act with a real cost (a
108
+ diverged store ref then makes unrelated `git fetch` calls exit 1). It belongs to the onboarding
109
+ skill that already asks consent before writing operational config, not to this engine.
110
+ - **Recovering from a refused sync.** The refusal names both tips. Read each side's log with
111
+ `git cat-file -p <tip>:events.jsonl`, then — as the single write-decider — rebuild one list holding
112
+ both sides' events and append it forward from the tip you keep. The store is append-only, so no
113
+ event is dropped by that reconcile; `sync` does not automate it, because choosing the surviving
114
+ order is a decision, not a merge.
115
+ - **Never configure `remote.<remote>.push`.** Setting it *replaces* the default branch push refspec,
116
+ so a plain `git push` silently stops sending the current branch while reporting
117
+ "Everything up-to-date". `sync` passes its push refspec as an argument and writes no push config.
118
+ - **`sync` is orphan-ref-only.** Under the in-tree backend it is a **no-op** that reports the active
119
+ backend — an in-tree store rides its branch and needs no ref transport.
53
120
  - `append edge` runs the **write-time cycle guard** first: a RAW edge that would close a loop is
54
121
  **rejected** unless `--override` is passed (a genuinely-discovered mutual dependency). A
55
122
  parent-child/discovered-from edge is never guarded — only RAW ("wait for") edges can cycle.