cyber-sdd 0.0.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 (133) hide show
  1. package/.claude-plugin/plugin.json +17 -0
  2. package/.codex-plugin/plugin.json +17 -0
  3. package/.plugin/plugin.json +17 -0
  4. package/README.md +159 -0
  5. package/agents/sdd-automaton.md +97 -0
  6. package/agents/sdd-impl-judge.md +214 -0
  7. package/agents/sdd-scanner.md +120 -0
  8. package/agents/sdd-spec-judge.md +224 -0
  9. package/agents/sdd-warden.md +101 -0
  10. package/package.json +24 -0
  11. package/skills/align-spec/README.md +20 -0
  12. package/skills/align-spec/SKILL.md +111 -0
  13. package/skills/align-spec/scripts/align-spec.mts +187 -0
  14. package/skills/architect-impl-governance/README.md +46 -0
  15. package/skills/architect-impl-governance/SKILL.md +45 -0
  16. package/skills/architect-spec-governance/README.md +48 -0
  17. package/skills/architect-spec-governance/SKILL.md +59 -0
  18. package/skills/blast-estimate/README.md +47 -0
  19. package/skills/blast-estimate/SKILL.md +133 -0
  20. package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
  21. package/skills/builder-impl-governance/README.md +47 -0
  22. package/skills/builder-impl-governance/SKILL.md +47 -0
  23. package/skills/builder-spec-governance/README.md +49 -0
  24. package/skills/builder-spec-governance/SKILL.md +36 -0
  25. package/skills/check-partition-quality/README.md +22 -0
  26. package/skills/check-partition-quality/SKILL.md +51 -0
  27. package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
  28. package/skills/check-plan-safety/README.md +17 -0
  29. package/skills/check-plan-safety/SKILL.md +60 -0
  30. package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
  31. package/skills/check-project-specs/README.md +19 -0
  32. package/skills/check-project-specs/SKILL.md +69 -0
  33. package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
  34. package/skills/check-scenario-overlap/README.md +19 -0
  35. package/skills/check-scenario-overlap/SKILL.md +74 -0
  36. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
  37. package/skills/check-spec-structure/README.md +17 -0
  38. package/skills/check-spec-structure/SKILL.md +66 -0
  39. package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
  40. package/skills/collision-ladder/README.md +18 -0
  41. package/skills/collision-ladder/SKILL.md +83 -0
  42. package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
  43. package/skills/combat-log-governance/README.md +13 -0
  44. package/skills/combat-log-governance/SKILL.md +257 -0
  45. package/skills/concept-index/README.md +13 -0
  46. package/skills/concept-index/SKILL.md +38 -0
  47. package/skills/concept-index/scripts/concept-index.mts +245 -0
  48. package/skills/discover-plans/README.md +16 -0
  49. package/skills/discover-plans/SKILL.md +74 -0
  50. package/skills/discover-plans/scripts/discover-plans.mts +212 -0
  51. package/skills/discover-specs/README.md +15 -0
  52. package/skills/discover-specs/SKILL.md +76 -0
  53. package/skills/discover-specs/scripts/discover-specs.mts +396 -0
  54. package/skills/doctrine-loop/README.md +15 -0
  55. package/skills/doctrine-loop/SKILL.md +97 -0
  56. package/skills/formation-loop/README.md +17 -0
  57. package/skills/formation-loop/SKILL.md +140 -0
  58. package/skills/gate-validation-governance/README.md +12 -0
  59. package/skills/gate-validation-governance/SKILL.md +87 -0
  60. package/skills/impl-producer-governance/README.md +48 -0
  61. package/skills/impl-producer-governance/SKILL.md +85 -0
  62. package/skills/init/README.md +27 -0
  63. package/skills/init/SKILL.md +68 -0
  64. package/skills/init/scripts/wire-statusline.mts +276 -0
  65. package/skills/lifecycle-governance/README.md +11 -0
  66. package/skills/lifecycle-governance/SKILL.md +168 -0
  67. package/skills/manage/README.md +9 -0
  68. package/skills/manage/SKILL.md +62 -0
  69. package/skills/manage-ignore/README.md +19 -0
  70. package/skills/manage-ignore/SKILL.md +52 -0
  71. package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
  72. package/skills/manage-scenario-bridge/README.md +20 -0
  73. package/skills/manage-scenario-bridge/SKILL.md +60 -0
  74. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
  75. package/skills/manage-spec-anchors/README.md +18 -0
  76. package/skills/manage-spec-anchors/SKILL.md +56 -0
  77. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
  78. package/skills/mission-graph/README.md +15 -0
  79. package/skills/mission-graph/SKILL.md +67 -0
  80. package/skills/mission-graph/scripts/mission-graph.mts +844 -0
  81. package/skills/oracle-spec-governance/README.md +45 -0
  82. package/skills/oracle-spec-governance/SKILL.md +45 -0
  83. package/skills/ownership-governance/README.md +65 -0
  84. package/skills/ownership-governance/SKILL.md +104 -0
  85. package/skills/pause-mission/README.md +18 -0
  86. package/skills/pause-mission/SKILL.md +112 -0
  87. package/skills/place-node/README.md +12 -0
  88. package/skills/place-node/SKILL.md +47 -0
  89. package/skills/place-node/scripts/place-node.mts +157 -0
  90. package/skills/plan-retirement/README.md +32 -0
  91. package/skills/plan-retirement/SKILL.md +90 -0
  92. package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
  93. package/skills/plugin-contract-governance/README.md +12 -0
  94. package/skills/plugin-contract-governance/SKILL.md +112 -0
  95. package/skills/remediation-governance/README.md +46 -0
  96. package/skills/remediation-governance/SKILL.md +78 -0
  97. package/skills/resolve-governances/README.md +18 -0
  98. package/skills/resolve-governances/SKILL.md +50 -0
  99. package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
  100. package/skills/resolve-tracking/SKILL.md +64 -0
  101. package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
  102. package/skills/resume-mission/README.md +12 -0
  103. package/skills/resume-mission/SKILL.md +53 -0
  104. package/skills/scaffold-project-spec/README.md +7 -0
  105. package/skills/scaffold-project-spec/SKILL.md +192 -0
  106. package/skills/sdd/README.md +7 -0
  107. package/skills/sdd/SKILL.md +92 -0
  108. package/skills/solution-producer-governance/README.md +9 -0
  109. package/skills/solution-producer-governance/SKILL.md +44 -0
  110. package/skills/spec-format-governance/README.md +73 -0
  111. package/skills/spec-format-governance/SKILL.md +114 -0
  112. package/skills/spec-gate/README.md +26 -0
  113. package/skills/spec-gate/SKILL.md +201 -0
  114. package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
  115. package/skills/spec-gate/scripts/check-suite.mts +501 -0
  116. package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
  117. package/skills/spec-producer-governance/README.md +7 -0
  118. package/skills/spec-producer-governance/SKILL.md +86 -0
  119. package/skills/spec-structure-governance/README.md +40 -0
  120. package/skills/spec-structure-governance/SKILL.md +169 -0
  121. package/skills/ssa-lowering/README.md +26 -0
  122. package/skills/ssa-lowering/SKILL.md +181 -0
  123. package/skills/start-mission/README.md +7 -0
  124. package/skills/start-mission/SKILL.md +115 -0
  125. package/skills/suite-format-governance/README.md +75 -0
  126. package/skills/suite-format-governance/SKILL.md +299 -0
  127. package/skills/suite-format-governance/references/rubric.md +313 -0
  128. package/skills/touch-set-correction/README.md +16 -0
  129. package/skills/touch-set-correction/SKILL.md +67 -0
  130. package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
  131. package/skills/verify-scenarios/README.md +17 -0
  132. package/skills/verify-scenarios/SKILL.md +109 -0
  133. package/skills/verify-scenarios/scripts/verify-scenarios.mts +386 -0
@@ -0,0 +1,49 @@
1
+ # builder-spec-governance
2
+
3
+ This is an internal SDD governance about the Builder's bar at the **spec gate**.
4
+
5
+ It answers one question: is this **capability** fully and testably specified? In SDD, a capability's
6
+ contract is written down twice — as a `spec.md` (the decisions it makes, drawn as a **control-flow
7
+ graph**, or CFG) and as a `.feature` suite (one test scenario per branch of that CFG). This bar
8
+ judges whether that contract is complete and checkable *before* anyone builds against it. It judges
9
+ the contract itself, read from the spec plus its suite — not how the document is written, which is a
10
+ different bar
11
+ (`spec-format-governance`).
12
+
13
+ It is one half of a matched pair: this bar asks "is the contract testable and covered?" at the spec
14
+ gate; its sibling **`builder-impl-governance`** asks "does the implementation meet that frozen
15
+ contract?" at the impl gate.
16
+
17
+ ## What it requires
18
+
19
+ | Requirement | What it means |
20
+ | --- | --- |
21
+ | **Every branch is covered** | Each edge of the capability's CFG has its scenario, and every guard/negative edge is paired with a positive companion. The scenario map is 1:1 in both directions — no orphan scenario, no uncovered edge. |
22
+ | **Every scenario is testable** | Each scenario asserts an observable outcome a check can confirm — a boolean, no "sometimes". A behavior the capability cannot expose cannot be specced. |
23
+ | **A graded subject is still a boolean** | A non-deterministic capability (one whose output varies run to run) still reaches a per-scenario boolean, through a rubric plus a threshold over N runs. The rubric form stays out of the boolean `.feature`, carried as a judge-only `@rubric` scenario. |
24
+
25
+ This is the SDD default for the `builder` spec bar; a plugin may bind its own per artifact-type, and
26
+ this one loads when the registry leaves `builder`/`spec` unbound.
27
+
28
+ ## Usage
29
+
30
+ One merged bar loaded by both faces at the **spec gate**:
31
+
32
+ - **spec-producer:** self-aligns to it while writing the testable suite
33
+ - **cold spec-judge:** grades coverage and testability against it
34
+
35
+ `producer ≠ judge` holds at the agent level — the same bar, two independent readers.
36
+
37
+ ## Related governances
38
+
39
+ This bar owns the testability and coverage of the capability's contract. Its neighbors own
40
+ everything around that:
41
+
42
+ - **`builder-impl-governance`** — the other half of the pair: whether the *implementation* meets the
43
+ frozen contract, judged at the impl gate. This bar freezes what must hold; that one verifies it held.
44
+ - **`spec-format-governance`** — the document's prose and section layout. That bar reads the
45
+ `spec.md` as a document; this bar reads the spec + suite as a contract.
46
+ - **`suite-format-governance`** — how the `.feature` suite itself is written, including the 1:1
47
+ scenario-map rule this bar enforces coverage against.
48
+
49
+ Internal SDD governance (`user-invocable: false`). Not triggered by users directly.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: builder-spec-governance
3
+ description: "Partial Skill: invoke by name only"
4
+ user-invocable: false
5
+ metadata:
6
+ actor: builder
7
+ gate: spec
8
+ compose: union
9
+ ---
10
+
11
+ # Builder-Spec Governance — the testability & coverage bar
12
+
13
+ The **Builder** bar at the **spec gate**: is this **capability** fully and testably specified? Judges
14
+ the capability's contract (read from its spec + suite), not the document's prose — that is
15
+ `sdd:spec-format-governance`. Loaded by both faces. The SDD default for the `builder` spec bar; a
16
+ plugin may bind its own, and this loads when the registry leaves `builder`/`spec` unbound.
17
+
18
+ ## The bar
19
+
20
+ - **Every branch of the capability is covered.** Each edge of its control-flow graph (CFG) has its
21
+ scenario, and every guard/negative edge is paired with a positive companion. The **scenario map
22
+ is 1:1** — no orphan scenario, no uncovered edge (`sdd:suite-format-governance`).
23
+ - **Every scenario is testable.** Each asserts an observable outcome a check can confirm — a boolean,
24
+ no "sometimes". A behavior the capability cannot expose cannot be specced.
25
+ - **A graded subject is still a boolean.** For a non-deterministic capability the contract reaches a
26
+ per-scenario boolean through a rubric + threshold over N runs; the rubric form stays out of the
27
+ boolean `.feature`, carried as a judge-only `@rubric` scenario.
28
+
29
+ ## Key points (read-check)
30
+
31
+ 1. **Every branch of the capability is covered** — every edge has its scenario, guards paired with
32
+ positives, the scenario map 1:1.
33
+ 2. **Every scenario is testable** — an observable boolean outcome; behavior the capability cannot
34
+ expose cannot be specced.
35
+ 3. **A graded subject still reaches a per-scenario boolean** via rubric + threshold; the rubric stays
36
+ out of the `.feature`.
@@ -0,0 +1,22 @@
1
+ # check-partition-quality
2
+
3
+ Internal SDD engine (`user-invocable: false`). Measures from a project's **git history** how much
4
+ parallel work its layout permits, and compares candidate layouts on the same evidence.
5
+
6
+ **Why it exists.** SDD's scheduler cuts one mission per spec-node, so a layout that scatters a
7
+ capability makes ordinary changes collide and the schedule serializes. That was long asserted rather
8
+ than measured; this produces the number, per project, so a layout decision rests on the project's own
9
+ history instead of doctrine.
10
+
11
+ **The headline is the parallelizable share** — how often two changes from history could run in
12
+ parallel. Every run also reports a shuffled control, because a partition that cannot beat chance
13
+ explains nothing.
14
+
15
+ **One design note worth keeping.** Two more obvious metrics — within-node co-change ratio, and mean
16
+ nodes touched per change — were tried first and **both preferred a layered layout**, because both
17
+ reward a coarser partition for being coarse. They are still printed, labelled as confounded, so the
18
+ mistake is visible rather than repeatable.
19
+
20
+ Runnable standalone; opt-in from `scaffold-project-spec` (detection mode, to inform the strategy
21
+ choice) and the formation loop (the layout-quality signal). Never a gate. Not triggered by users
22
+ directly.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: check-partition-quality
3
+ description: "Partial Skill: invoke by name only — project-spec/partition-quality's engine: measures from git history how much parallel work a layout permits, and compares candidate layouts. Opt-in from scaffold-project-spec and the formation loop, and runnable standalone; not triggered by users directly."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # Check Partition Quality
10
+
11
+ Measures, from a project's **own git history**, how much parallel work its layout actually permits —
12
+ and compares candidate layouts on the same evidence. The mission scheduler cuts **one mission per
13
+ spec-node**, so two changes touching a shared node must serialize; this reports how often that
14
+ happens.
15
+
16
+ Read-only and advisory: it moves no file, writes nothing, and renders **no verdict**. Layout is the
17
+ owner's call (`sdd:spec-structure-governance` — adoption over purity).
18
+
19
+ ## Run it
20
+
21
+ ```bash
22
+ node "<skill>/scripts/check-partition-quality.mts" \
23
+ --repo <path> --scope <dir> [--partition <name>]... [--floor N] [--limit N] [--format json]
24
+ ```
25
+
26
+ - `--scope` — restrict to a subtree (e.g. `src`, `plugins/sdd/skills`). Files outside it are ignored.
27
+ - `--partition` — repeatable; compare candidates on the same commits. Built in:
28
+ `top-folder` (the capability cut) · `second-folder` (capability/unit) · `role` (artifact role — the
29
+ layered analogue, useful as a contrast) · `single` (the degenerate floor: no parallel work).
30
+ Default compares `top-folder` against `role`.
31
+ - `--floor` — minimum usable multi-file commits before a rate is emitted (default 20).
32
+
33
+ ## Reading the output
34
+
35
+ **The headline is the parallelizable share** — of two changes drawn from history, how often they
36
+ *could* run in parallel. Higher is better.
37
+
38
+ **Check the control before believing it.** Every run reports the same metric over a **shuffled**
39
+ partition of identical node sizes. A partition that does not beat its shuffle explains nothing, and
40
+ the run says so.
41
+
42
+ **The diagnostics are confounded — never headline them.** `within-node co-change` and `mean nodes
43
+ touched` both reward a **coarser** partition for being coarse: a single-node partition scores a
44
+ *perfect* 1 on each while permitting zero parallel work. They are printed only so a reader can see
45
+ why they are not used. Two engines built on them would recommend a layered layout.
46
+
47
+ ## Boundaries
48
+
49
+ Measurement only — no file is moved, no spec written, no layout approved or rejected. Thin history is
50
+ **reported, never scored**: below the floor it emits no rate rather than a confident number drawn
51
+ from noise. It reads `git log` and nothing else; no node body, no spec frontmatter.
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+ // check-partition-quality — project-spec/partition-quality's concrete engine. Measures, from a
3
+ // project's own git history, how much parallel work its layout permits, and compares candidate
4
+ // layouts on the same evidence (see this skill's README.md).
5
+ //
6
+ // THE METRIC IS COLLISION RATE, AND THAT CHOICE IS LOAD-BEARING. The scheduler cuts one mission per
7
+ // spec-node, so two changes touching a shared node must serialize. Collision rate is the share of
8
+ // change pairs that do. The headline is its complement — the parallelizable share.
9
+ //
10
+ // Two obvious alternatives were tried first and BOTH preferred a layered partition (the wrong
11
+ // answer), because both are confounded by node count:
12
+ // - within-node co-change ratio — a coarser partition scores well for being coarse
13
+ // - mean nodes touched per change — a single-node partition scores PERFECTLY while permitting
14
+ // zero parallel work
15
+ // They are computed and reported here as DIAGNOSTICS, explicitly labelled, so that a later reader
16
+ // does not "simplify" the engine back onto one of them. Do not headline them.
17
+ //
18
+ // Read-only: it runs `git log`, writes nothing, and renders no verdict — layout is the owner's call.
19
+ // No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions are exported for
20
+ // node:test; running the file directly drives the CLI.
21
+
22
+ import { execFileSync } from 'node:child_process'
23
+
24
+ /** Below this many usable multi-file commits, a rate would be noise — report, never score. */
25
+ export const DEFAULT_FLOOR = 20
26
+
27
+ export interface Change {
28
+ /** In-scope files touched by one commit. */
29
+ files: string[]
30
+ }
31
+
32
+ /** A partition names the node each file belongs to; `undefined` drops the file from the measure. */
33
+ export type Partition = (file: string) => string | undefined
34
+
35
+ export interface Measurement {
36
+ nodes: number
37
+ pairs: number
38
+ collisionRate: number
39
+ parallelizableShare: number
40
+ control: number
41
+ /** Positive when the partition explains more than a shuffle of the same node sizes. */
42
+ marginOverControl: number
43
+ explainsNothing: boolean
44
+ diagnostics: {
45
+ /** CONFOUNDED by node count — a coarser partition scores higher. Never headline. */
46
+ withinNodeCoChangeRatio: number
47
+ /** CONFOUNDED by node count — a single-node partition scores a perfect 1. Never headline. */
48
+ meanNodesTouched: number
49
+ }
50
+ }
51
+
52
+ export interface ThinHistory {
53
+ thin: true
54
+ usableCommits: number
55
+ floor: number
56
+ }
57
+
58
+ // ── Reading history ──────────────────────────────────────────────────────────
59
+ // Only multi-file changes inform the measure: a single-file commit contributes no pair, because one
60
+ // file can collide with nothing.
61
+
62
+ export function parseGitLog(stdout: string, inScope: (f: string) => boolean): Change[] {
63
+ const out: Change[] = []
64
+ let cur: string[] | null = null
65
+ for (const line of stdout.split('\n')) {
66
+ const t = line.trimEnd()
67
+ if (t === '') {
68
+ if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] })
69
+ cur = null
70
+ continue
71
+ }
72
+ if (/^[0-9a-f]{40}$/.test(t)) {
73
+ if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] })
74
+ cur = []
75
+ continue
76
+ }
77
+ if (cur !== null && inScope(t)) cur.push(t)
78
+ }
79
+ if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] })
80
+ return out.filter((c) => c.files.length > 1)
81
+ }
82
+
83
+ export function readHistory(repo: string, inScope: (f: string) => boolean, limit = 4000): Change[] {
84
+ const stdout = execFileSync('git', ['-C', repo, 'log', `-${limit}`, '--name-only', '--pretty=format:%H'], {
85
+ encoding: 'utf8',
86
+ maxBuffer: 256 * 1024 * 1024,
87
+ })
88
+ return parseGitLog(stdout, inScope)
89
+ }
90
+
91
+ // ── The metric ───────────────────────────────────────────────────────────────
92
+
93
+ /** The set of nodes one change touches. A change touching none drops out of the measure. */
94
+ function nodesOf(c: Change, p: Partition): Set<string> {
95
+ const s = new Set<string>()
96
+ for (const f of c.files) {
97
+ const n = p(f)
98
+ if (n !== undefined) s.add(n)
99
+ }
100
+ return s
101
+ }
102
+
103
+ export function collisionRate(changes: Change[], p: Partition): { rate: number; pairs: number } {
104
+ const sets = changes.map((c) => nodesOf(c, p)).filter((s) => s.size > 0)
105
+ let pairs = 0
106
+ let collided = 0
107
+ for (let i = 0; i < sets.length; i++) {
108
+ for (let j = i + 1; j < sets.length; j++) {
109
+ pairs++
110
+ const a = sets[i] as Set<string>
111
+ const b = sets[j] as Set<string>
112
+ for (const n of a) {
113
+ if (b.has(n)) {
114
+ collided++
115
+ break
116
+ }
117
+ }
118
+ }
119
+ }
120
+ return { rate: pairs === 0 ? 0 : collided / pairs, pairs }
121
+ }
122
+
123
+ // ── The control ──────────────────────────────────────────────────────────────
124
+ // The same measurement over a SHUFFLED partition of identical node sizes. A partition whose rate
125
+ // matches its shuffle explains nothing, and the headline should not be trusted. Deterministic seed:
126
+ // the run must be reproducible.
127
+
128
+ function mulberry32(seed: number): () => number {
129
+ let a = seed >>> 0
130
+ return () => {
131
+ a = (a + 0x6d2b79f5) >>> 0
132
+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
133
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
134
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
135
+ }
136
+ }
137
+
138
+ export function shuffledControl(changes: Change[], p: Partition, seed = 7): number {
139
+ const files = [...new Set(changes.flatMap((c) => c.files))].filter((f) => p(f) !== undefined).sort()
140
+ const labels = files.map((f) => p(f) as string)
141
+ const rnd = mulberry32(seed)
142
+ for (let i = labels.length - 1; i > 0; i--) {
143
+ const j = Math.floor(rnd() * (i + 1))
144
+ const li = labels[i] as string
145
+ const lj = labels[j] as string
146
+ labels[i] = lj
147
+ labels[j] = li
148
+ }
149
+ const m = new Map(files.map((f, i) => [f, labels[i] as string]))
150
+ return collisionRate(changes, (f) => m.get(f)).rate
151
+ }
152
+
153
+ /**
154
+ * Rename `diagnostics` to `confoundedDiagnostics` on the way out to JSON.
155
+ *
156
+ * The text render carries the warning inline; a JSON consumer sees only keys, so a plain
157
+ * `diagnostics` object hands over two numbers indistinguishable from the headline. Renaming the key
158
+ * beats adding a note field: a consumer cannot read these numbers without typing the word
159
+ * "confounded", whereas a sibling note is trivially skipped.
160
+ */
161
+ export function toJson([name, m]: [string, Measurement | ThinHistory]): [string, unknown] {
162
+ if (!('diagnostics' in m)) return [name, m]
163
+ const { diagnostics, ...rest } = m
164
+ return [name, { ...rest, confoundedDiagnostics: diagnostics }]
165
+ }
166
+
167
+ // ── Confounded diagnostics — labelled, never the headline ────────────────────
168
+
169
+ export function withinNodeCoChangeRatio(changes: Change[], p: Partition): number {
170
+ let within = 0
171
+ let total = 0
172
+ for (const c of changes) {
173
+ const ns = c.files.map((f) => p(f)).filter((n): n is string => n !== undefined)
174
+ for (let i = 0; i < ns.length; i++) {
175
+ for (let j = i + 1; j < ns.length; j++) {
176
+ total++
177
+ if (ns[i] === ns[j]) within++
178
+ }
179
+ }
180
+ }
181
+ return total === 0 ? 0 : within / total
182
+ }
183
+
184
+ export function meanNodesTouched(changes: Change[], p: Partition): number {
185
+ const counts = changes.map((c) => nodesOf(c, p).size).filter((n) => n > 0)
186
+ return counts.length === 0 ? 0 : counts.reduce((a, b) => a + b, 0) / counts.length
187
+ }
188
+
189
+ // ── Measure ──────────────────────────────────────────────────────────────────
190
+
191
+ export function measure(changes: Change[], p: Partition, floor = DEFAULT_FLOOR): Measurement | ThinHistory {
192
+ const usable = changes.filter((c) => nodesOf(c, p).size > 0)
193
+ if (usable.length < floor) return { thin: true, usableCommits: usable.length, floor }
194
+ const { rate, pairs } = collisionRate(usable, p)
195
+ const control = shuffledControl(usable, p)
196
+ const margin = control - rate // lower collision than the shuffle is the improvement
197
+ return {
198
+ nodes: new Set(usable.flatMap((c) => [...nodesOf(c, p)])).size,
199
+ pairs,
200
+ collisionRate: rate,
201
+ parallelizableShare: 1 - rate,
202
+ control,
203
+ marginOverControl: margin,
204
+ explainsNothing: margin <= 0,
205
+ diagnostics: {
206
+ withinNodeCoChangeRatio: withinNodeCoChangeRatio(usable, p),
207
+ meanNodesTouched: meanNodesTouched(usable, p),
208
+ },
209
+ }
210
+ }
211
+
212
+ export function isThin(m: Measurement | ThinHistory): m is ThinHistory {
213
+ return (m as ThinHistory).thin === true
214
+ }
215
+
216
+ // ── Built-in candidate partitions ────────────────────────────────────────────
217
+ // Named so a caller can compare "what the tree does now" against "what a capability cut would do"
218
+ // without writing code.
219
+
220
+ export const PARTITIONS: Record<string, (scope: string) => Partition> = {
221
+ /** Top-level folder under the scope — the capability cut for a screaming layout. */
222
+ 'top-folder': (scope) => (f) => {
223
+ const rel = f.startsWith(scope) ? f.slice(scope.length).replace(/^\//, '') : undefined
224
+ if (rel === undefined) return undefined
225
+ const seg = rel.split('/')[0]
226
+ return seg === undefined || seg === '' || !rel.includes('/') ? undefined : seg
227
+ },
228
+ /** Second-level folder — a finer cut, for a capability/unit tree. */
229
+ 'second-folder': (scope) => (f) => {
230
+ const rel = f.startsWith(scope) ? f.slice(scope.length).replace(/^\//, '') : undefined
231
+ if (rel === undefined) return undefined
232
+ const p = rel.split('/')
233
+ return p.length > 2 ? `${p[0]}/${p[1]}` : undefined
234
+ },
235
+ /** File extension / artifact role — the layered analogue, useful as a contrast candidate. */
236
+ role: (scope) => (f) => {
237
+ if (!f.startsWith(scope)) return undefined
238
+ const n = f.split('/').pop() ?? ''
239
+ const dot = n.indexOf('.')
240
+ return dot === -1 ? n : n.slice(dot + 1)
241
+ },
242
+ /** Everything in one node — the degenerate floor: no parallel work is possible. */
243
+ single: (scope) => (f) => (f.startsWith(scope) ? 'all' : undefined),
244
+ }
245
+
246
+ // ── Render ───────────────────────────────────────────────────────────────────
247
+
248
+ function pct(n: number): string {
249
+ return `${(n * 100).toFixed(1)}%`
250
+ }
251
+
252
+ export function renderOne(label: string, m: Measurement | ThinHistory): string {
253
+ if (isThin(m)) {
254
+ return ` ${label}: history too thin to measure — ${m.usableCommits} usable multi-file commits, floor ${m.floor}. No rate emitted.`
255
+ }
256
+ const lines = [
257
+ ` ${label}`,
258
+ ` parallelizable share : ${pct(m.parallelizableShare)} ← the headline`,
259
+ ` collision rate : ${pct(m.collisionRate)} over ${m.pairs} change pairs, ${m.nodes} nodes`,
260
+ ` shuffled control : ${pct(m.control)} (margin ${m.marginOverControl >= 0 ? '+' : ''}${pct(m.marginOverControl)})`,
261
+ ]
262
+ if (m.collisionRate >= 1) {
263
+ lines.push(' ⚠ every change pair collides — this partition permits no parallel work')
264
+ }
265
+ if (m.explainsNothing) {
266
+ lines.push(' ⚠ this partition explains no more than chance — do not trust the headline')
267
+ }
268
+ lines.push(
269
+ ` diagnostics (CONFOUNDED by node count — not the headline): within-node co-change ${pct(m.diagnostics.withinNodeCoChangeRatio)}, mean nodes touched ${m.diagnostics.meanNodesTouched.toFixed(2)}`,
270
+ )
271
+ return lines.join('\n')
272
+ }
273
+
274
+ export function render(results: [string, Measurement | ThinHistory][], repo: string, scope: string): string {
275
+ const out = [`check-partition-quality: repo=${repo} scope=${scope || '(whole repo)'}`]
276
+ for (const [label, m] of results) out.push(renderOne(label, m))
277
+ if (results.length > 1) {
278
+ const scored = results.filter(([, m]) => !isThin(m)) as [string, Measurement][]
279
+ if (scored.length > 1) {
280
+ const best = scored.reduce((a, b) => (b[1].parallelizableShare > a[1].parallelizableShare ? b : a))
281
+ out.push(`\nOn this history, "${best[0]}" permits the most parallel work (${pct(best[1].parallelizableShare)}).`)
282
+ }
283
+ }
284
+ out.push('note: a measurement, not a verdict — it moves no file and gates nothing; layout is the owner’s call')
285
+ return out.join('\n')
286
+ }
287
+
288
+ // ── CLI ──────────────────────────────────────────────────────────────────────
289
+
290
+ /**
291
+ * The effects `main` reaches the outside world through. Dependencies enter at the CLI boundary and
292
+ * nowhere below it: everything under `main` is pure over the history it is handed.
293
+ */
294
+ export type Context = { readHistory: typeof readHistory }
295
+
296
+ export function main(argv: string[], context: Context = { readHistory }): number {
297
+ let repo = '.'
298
+ let scope = ''
299
+ let floor = DEFAULT_FLOOR
300
+ let limit = 4000
301
+ let format: 'text' | 'json' = 'text'
302
+ const candidates: string[] = []
303
+ for (let i = 0; i < argv.length; i++) {
304
+ const a = argv[i]
305
+ if (a === '--repo') repo = argv[++i] ?? '.'
306
+ else if (a === '--scope') scope = argv[++i] ?? ''
307
+ else if (a === '--floor') floor = Number(argv[++i] ?? DEFAULT_FLOOR)
308
+ else if (a === '--limit') limit = Number(argv[++i] ?? 4000)
309
+ else if (a === '--format') format = (argv[++i] as 'text' | 'json') ?? 'text'
310
+ else if (a === '--partition') candidates.push(argv[++i] ?? '')
311
+ }
312
+ if (candidates.length === 0) candidates.push('top-folder', 'role')
313
+ for (const c of candidates) {
314
+ if (!(c in PARTITIONS)) {
315
+ process.stderr.write(`unknown --partition "${c}" — known: ${Object.keys(PARTITIONS).join(', ')}\n`)
316
+ return 1
317
+ }
318
+ }
319
+ // Read ONCE, then measure every candidate over that one history — the comparison is only
320
+ // meaningful if each candidate is scored against the same commits and the same scope.
321
+ const changes = context.readHistory(repo, (f) => (scope ? f.startsWith(scope) : true), limit)
322
+ const results = candidates.map(
323
+ (c) =>
324
+ [c, measure(changes, (PARTITIONS[c] as (s: string) => Partition)(scope), floor)] as [
325
+ string,
326
+ Measurement | ThinHistory,
327
+ ],
328
+ )
329
+ if (format === 'json') process.stdout.write(`${JSON.stringify(Object.fromEntries(results.map(toJson)), null, 2)}\n`)
330
+ else process.stdout.write(`${render(results, repo, scope)}\n`)
331
+ return 0
332
+ }
333
+
334
+ if (import.meta.url === `file://${process.argv[1]}`) {
335
+ process.exit(main(process.argv.slice(2)))
336
+ }
@@ -0,0 +1,17 @@
1
+ # check-plan-safety
2
+
3
+ Internal SDD skill — the concrete guard engine for the **plan brief's safe-to-publish floor**. Scans
4
+ the tracked, portable handoff artifacts under `.agents/plans` (the `*.plan.md` brief + sibling design
5
+ docs) for machine-local references that must never enter git history.
6
+
7
+ ```bash
8
+ node scripts/check-plan-safety.mts --root . # audit (TOON finding set)
9
+ node scripts/check-plan-safety.mts --root . --check # CI guard (fails on any leak)
10
+ node scripts/check-plan-safety.mts --path <file> --check # check one explicit file
11
+ ```
12
+
13
+ Flags a **home-directory absolute path** (`/home/<user>/…`, `/Users/<user>/…`, `C:\Users\<user>\…`)
14
+ or a **`$HOME`/`$USER` expansion** — each leaks an OS username and breaks portability. A bare `~/` is
15
+ deliberately not flagged (no username; legitimate in design prose). The plan-brief analog of the
16
+ combat log's floor. Read-only; writes nothing. See [`SKILL.md`](./SKILL.md) for the full contract.
17
+ Not user-invocable.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: check-plan-safety
3
+ description: "Partial Skill: invoke by name only — plan-brief/check-plan-safety's guard engine against machine-local path leaks in tracked plan artifacts — the CI guard, not triggered by users directly."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # Check Plan Safety
10
+
11
+ The concrete engine for the **plan brief's safe-to-publish floor**. The plan brief
12
+ (`.agents/plans/<cr-ref>.plan.md`) and its sibling design docs are **tracked, portable** handoff
13
+ artifacts — committed at handoff and read by any later session, agent, or checkout. This engine
14
+ guards that floor mechanically: it scans those `*.md` files for **machine-local references** that
15
+ must never enter git history. It carries a self-contained `.mts` script (the repo's node-≥23.6 /
16
+ no-deps convention), and is the plan-brief analog of the combat log's safe-to-publish floor
17
+ (`combat-log-governance`) — the same "never committed: absolute paths, OS usernames" rule, extended
18
+ from the ledger to the brief.
19
+
20
+ ## What it flags
21
+
22
+ - **home-abs-path** — a home-directory absolute path: `/home/<user>/…`, `/Users/<user>/…`, or a
23
+ Windows profile `C:\Users\<user>\…`. Leaks the OS username (privacy) **and** resolves on no other
24
+ checkout (portability).
25
+ - **env-home** — a shell expansion of the home dir: `$HOME`, `${HOME}`, `%USERPROFILE%`, `%HOMEPATH%`.
26
+ - **env-user** — a shell expansion of the identity: `$USER`, `${USER}`, `%USERNAME%`.
27
+
28
+ Every finding is **blocking** — a leak is a leak. The fix is to bring the referenced content into
29
+ the repo and reference it **repo-relative** (e.g. copy a `~/.claude/plans/` design doc to
30
+ `.agents/plans/<cr-ref>.design.md`), never to link a machine-local path.
31
+
32
+ ### What it deliberately does NOT flag
33
+
34
+ A bare `~/` is **not** a leak — it carries no username and legitimately appears in design prose
35
+ describing home-rooted feature paths (a tool's own `~/.<tool>/` data root). `$HOMEBREW` / `$USERDATA`
36
+ (a different variable that merely starts with `HOME`/`USER`) are not flagged either.
37
+
38
+ ## Run the scan
39
+
40
+ ```bash
41
+ node "<skill>/scripts/check-plan-safety.mts" [--root .] [--path <file>]... [--check] [--format toon|json]
42
+ ```
43
+
44
+ - Default `--root` is the current directory; default `--format` is **TOON** (the token-efficient
45
+ tabular form). Absent `--path`, it scans every `*.md` under `<root>/.agents/plans`.
46
+ - **`--path <file>`** (repeatable) scans an explicit file set instead of the plan dir — used by
47
+ `pause-mission` to check the one brief it is about to commit.
48
+ - **Audit mode** (default) emits the finding set (`file, line, kind, token`) and exits **0**.
49
+ - **`--check`** (CI guard) exits **non-zero** iff any leak is found and **writes nothing** else.
50
+ Wired into `verify:specs` (`node …/check-plan-safety.mts --root . --check`).
51
+
52
+ When `node` is absent, an agent performs the same derivation by hand: grep the plan `*.md` files for
53
+ `/home/`, `/Users/`, `C:\Users\`, `$HOME`, and `$USER`.
54
+
55
+ ## Boundaries
56
+
57
+ Read-only — it writes nothing and acts on no finding. It does not fix a leak (the author scrubs it),
58
+ does not check gate legality (`spec-gate`), and does not audit node-shape (`check-spec-structure`).
59
+ It is loaded by the `manage` gateway (Audit & align) for an on-demand scan and by `pause-mission`
60
+ before a checkpoint commit.