cyber-sdd 0.6.0 → 0.7.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 (137) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +2 -2
  3. package/.plugin/plugin.json +1 -1
  4. package/agents/sdd-automaton.md +2 -3
  5. package/agents/sdd-impl-judge.md +5 -4
  6. package/agents/sdd-scanner.md +1 -1
  7. package/agents/sdd-spec-judge.md +3 -3
  8. package/agents/sdd-warden.md +8 -0
  9. package/dist/check-project-specs/scripts/check-project-specs.js +1 -0
  10. package/package.json +2 -2
  11. package/skills/align-spec/README.md +3 -3
  12. package/skills/align-spec/SKILL.md +1 -1
  13. package/skills/align-spec/scripts/align-spec.bundle.mjs +6753 -0
  14. package/skills/align-spec/scripts/align-spec.mjs +12 -0
  15. package/skills/architect-impl-governance/SKILL.md +3 -1
  16. package/skills/architect-spec-governance/SKILL.md +5 -1
  17. package/skills/blast-estimate/README.md +1 -1
  18. package/skills/blast-estimate/SKILL.md +1 -1
  19. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +593 -0
  20. package/skills/blast-estimate/scripts/blast-estimate.mjs +12 -0
  21. package/skills/check-field-mandates/SKILL.md +1 -1
  22. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +251 -0
  23. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +12 -0
  24. package/skills/check-partition-quality/SKILL.md +1 -1
  25. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +233 -0
  26. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +12 -0
  27. package/skills/check-plan-safety/README.md +3 -3
  28. package/skills/check-plan-safety/SKILL.md +1 -1
  29. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +107 -0
  30. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +12 -0
  31. package/skills/check-plugin-manifests/SKILL.md +1 -1
  32. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +248 -0
  33. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +12 -0
  34. package/skills/check-project-specs/README.md +3 -3
  35. package/skills/check-project-specs/SKILL.md +2 -2
  36. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +584 -0
  37. package/skills/check-project-specs/scripts/check-project-specs.mjs +12 -0
  38. package/skills/check-project-specs/scripts/check-project-specs.mts +1 -0
  39. package/skills/check-retired-terms/README.md +2 -2
  40. package/skills/check-retired-terms/SKILL.md +2 -2
  41. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +184 -0
  42. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +12 -0
  43. package/skills/check-scenario-overlap/README.md +2 -2
  44. package/skills/check-scenario-overlap/SKILL.md +2 -3
  45. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +175 -0
  46. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +12 -0
  47. package/skills/check-spec-references/README.md +1 -1
  48. package/skills/check-spec-references/SKILL.md +1 -1
  49. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +285 -0
  50. package/skills/check-spec-references/scripts/check-spec-references.mjs +12 -0
  51. package/skills/check-spec-structure/README.md +2 -2
  52. package/skills/check-spec-structure/SKILL.md +1 -1
  53. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +241 -0
  54. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +12 -0
  55. package/skills/collision-ladder/SKILL.md +3 -3
  56. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +7041 -0
  57. package/skills/collision-ladder/scripts/collision-ladder.mjs +12 -0
  58. package/skills/collision-ladder/scripts/collision-ladder.mts +4 -2
  59. package/skills/combat-log-governance/SKILL.md +1 -1
  60. package/skills/concept-index/README.md +2 -2
  61. package/skills/concept-index/SKILL.md +1 -1
  62. package/skills/concept-index/scripts/concept-index.bundle.mjs +181 -0
  63. package/skills/concept-index/scripts/concept-index.mjs +12 -0
  64. package/skills/discover-plans/README.md +2 -2
  65. package/skills/discover-plans/SKILL.md +1 -1
  66. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +135 -0
  67. package/skills/discover-plans/scripts/discover-plans.mjs +12 -0
  68. package/skills/discover-specs/README.md +1 -1
  69. package/skills/discover-specs/SKILL.md +1 -1
  70. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +319 -0
  71. package/skills/discover-specs/scripts/discover-specs.mjs +12 -0
  72. package/skills/doctrine-loop/SKILL.md +2 -2
  73. package/skills/formation-loop/SKILL.md +11 -4
  74. package/skills/gate-validation-governance/SKILL.md +1 -1
  75. package/skills/init/README.md +3 -3
  76. package/skills/init/SKILL.md +3 -3
  77. package/skills/init/scripts/wire-statusline.bundle.mjs +166 -0
  78. package/skills/init/scripts/wire-statusline.mjs +12 -0
  79. package/skills/manage-ignore/README.md +5 -5
  80. package/skills/manage-ignore/SKILL.md +1 -1
  81. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +244 -0
  82. package/skills/manage-ignore/scripts/manage-ignore.mjs +12 -0
  83. package/skills/manage-scenario-bridge/README.md +3 -3
  84. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  85. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +237 -0
  86. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +12 -0
  87. package/skills/manage-spec-anchors/README.md +4 -4
  88. package/skills/manage-spec-anchors/SKILL.md +1 -1
  89. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +308 -0
  90. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +12 -0
  91. package/skills/mission-graph/SKILL.md +9 -9
  92. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +1038 -0
  93. package/skills/mission-graph/scripts/mission-graph.mjs +12 -0
  94. package/skills/pause-mission/SKILL.md +1 -1
  95. package/skills/place-node/README.md +1 -1
  96. package/skills/place-node/SKILL.md +1 -1
  97. package/skills/place-node/scripts/place-node.bundle.mjs +115 -0
  98. package/skills/place-node/scripts/place-node.mjs +12 -0
  99. package/skills/plan-retirement/SKILL.md +1 -1
  100. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +121 -0
  101. package/skills/plan-retirement/scripts/retire-plans.mjs +12 -0
  102. package/skills/plan-retirement/scripts/retire-plans.mts +6 -0
  103. package/skills/plugin-contract-governance/SKILL.md +5 -12
  104. package/skills/resolve-governances/README.md +1 -1
  105. package/skills/resolve-governances/SKILL.md +1 -1
  106. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +350 -0
  107. package/skills/resolve-governances/scripts/resolve-governances.mjs +12 -0
  108. package/skills/resolve-governances/scripts/resolve-governances.mts +1 -1
  109. package/skills/resolve-tracking/SKILL.md +1 -1
  110. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +150 -0
  111. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +12 -0
  112. package/skills/scaffold-project-spec/SKILL.md +2 -2
  113. package/skills/sdd/SKILL.md +1 -1
  114. package/skills/solution-producer-governance/README.md +1 -1
  115. package/skills/solution-producer-governance/SKILL.md +1 -1
  116. package/skills/spec-gate/SKILL.md +5 -5
  117. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +420 -0
  118. package/skills/spec-gate/scripts/check-spec-state.mjs +12 -0
  119. package/skills/spec-gate/scripts/check-suite.bundle.mjs +6498 -0
  120. package/skills/spec-gate/scripts/check-suite.mjs +12 -0
  121. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +6577 -0
  122. package/skills/spec-gate/scripts/classify-edit-class.mjs +12 -0
  123. package/skills/spec-producer-governance/README.md +1 -1
  124. package/skills/spec-producer-governance/SKILL.md +3 -3
  125. package/skills/spec-structure-governance/README.md +4 -2
  126. package/skills/spec-structure-governance/SKILL.md +42 -7
  127. package/skills/start-mission/README.md +1 -1
  128. package/skills/start-mission/SKILL.md +6 -6
  129. package/skills/suite-format-governance/references/rubric.md +1 -1
  130. package/skills/touch-set-correction/SKILL.md +2 -2
  131. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +6655 -0
  132. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +12 -0
  133. package/skills/touch-set-correction/scripts/touch-set-correction.mts +12 -3
  134. package/skills/verify-scenarios/README.md +3 -3
  135. package/skills/verify-scenarios/SKILL.md +3 -3
  136. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +6537 -0
  137. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +12 -0
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ //#region skills/discover-specs/scripts/discover-specs.mts
6
+ const LIFECYCLE_STATUSES = /* @__PURE__ */ new Set([
7
+ "draft",
8
+ "approved",
9
+ "implemented",
10
+ "deprecated"
11
+ ]);
12
+ const ROOT_PROJECT_NAME = "repo";
13
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
14
+ "node_modules",
15
+ ".git",
16
+ "dist",
17
+ ".turbo",
18
+ ".next",
19
+ "coverage"
20
+ ]);
21
+ /**
22
+ * A directory that is itself a checkout is a DIFFERENT repository's tree, not part of this corpus.
23
+ *
24
+ * A name blocklist cannot express this: it can only say "this is called X", never "this is a
25
+ * boundary". `SKIP_DIRS` already holds `.git`, so the walk skipped the METADATA directory while
26
+ * descending straight into the checkout that directory marks — one level off from where the guard
27
+ * was needed. Agent-harness worktree isolation checks this repo out inside itself, and the scan then
28
+ * found the whole corpus once per worktree: 38 spec files where 10 exist, and every corpus-wide
29
+ * guard silently auditing a tree nobody has.
30
+ *
31
+ * `.git` is a DIRECTORY in a clone and a FILE in a worktree or submodule, so both forms count.
32
+ */
33
+ function isNestedCheckout(abs) {
34
+ return existsSync(join(abs, ".git"));
35
+ }
36
+ const ANCHORS_CONFIG = ".agents/sdd/spec-anchors.toml";
37
+ function parseFrontmatter(text) {
38
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
39
+ if (!m) return null;
40
+ const fm = { approval: {} };
41
+ let gate = null;
42
+ for (const raw of m[1].split("\n")) {
43
+ const line = raw.replace(/\r$/, "");
44
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
45
+ const indent = line.length - line.trimStart().length;
46
+ const [key, ...rest] = line.trim().split(":");
47
+ const value = rest.join(":").trim();
48
+ if (indent === 0) {
49
+ gate = null;
50
+ if (key === "status") fm.status = unquote(value);
51
+ else if (key === "project-path") fm.projectPath = unquote(value);
52
+ else if (key === "name") fm.name = unquote(value);
53
+ else if (key === "approval") gate = null;
54
+ } else if (indent === 2 && value === "" && insideApproval(m[1], line)) gate = key;
55
+ else if (indent >= 4 && key === "verdict" && gate) fm.approval[gate] = unquote(value);
56
+ }
57
+ return fm;
58
+ }
59
+ function insideApproval(block, line) {
60
+ const lines = block.split("\n");
61
+ const idx = lines.indexOf(line);
62
+ for (let i = idx - 1; i >= 0; i--) {
63
+ const l = lines[i].replace(/\r$/, "");
64
+ if (l.trim() === "" || l.trim().startsWith("#")) continue;
65
+ if (l.length - l.trimStart().length === 0) return l.trim().replace(/:.*$/, "") === "approval";
66
+ }
67
+ return false;
68
+ }
69
+ function unquote(v) {
70
+ return v.replace(/^["']|["']$/g, "");
71
+ }
72
+ function classifyLocation(relPath) {
73
+ const p = relPath.replace(/\\/g, "/");
74
+ if (p === ".agents/spec/spec.md") return {
75
+ pattern: "root-single",
76
+ locationDir: ""
77
+ };
78
+ const multi = /^\.agents\/specs\/([^/]+)\/spec\.md$/.exec(p);
79
+ if (multi) return {
80
+ pattern: "root-multi",
81
+ locationDir: multi[1]
82
+ };
83
+ const nested = /^(.+)\/\.agents\/spec\/spec\.md$/.exec(p);
84
+ if (nested) return {
85
+ pattern: "nested",
86
+ locationDir: nested[1]
87
+ };
88
+ return null;
89
+ }
90
+ function deriveName(loc, fm) {
91
+ if (fm.name) return {
92
+ name: fm.name,
93
+ nameSource: "declared"
94
+ };
95
+ if (loc.pattern === "root-single") return {
96
+ name: ROOT_PROJECT_NAME,
97
+ nameSource: "derived"
98
+ };
99
+ if (loc.pattern === "root-multi") return {
100
+ name: loc.locationDir,
101
+ nameSource: "derived"
102
+ };
103
+ if (loc.pattern === "extra" && loc.capturedName) return {
104
+ name: loc.capturedName,
105
+ nameSource: "derived"
106
+ };
107
+ return {
108
+ name: loc.locationDir.split("/").pop() ?? loc.locationDir,
109
+ nameSource: "guessed"
110
+ };
111
+ }
112
+ function discoverSpecFiles(root) {
113
+ const found = [];
114
+ const walk = (relDir) => {
115
+ let entries;
116
+ try {
117
+ entries = readdirSync(join(root, relDir), { withFileTypes: true });
118
+ } catch {
119
+ return;
120
+ }
121
+ for (const e of entries) {
122
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
123
+ const childRel = relDir ? `${relDir}/${e.name}` : e.name;
124
+ if (isNestedCheckout(join(root, childRel))) continue;
125
+ if (e.name === ".agents") {
126
+ probeAgents(root, childRel, found);
127
+ continue;
128
+ }
129
+ walk(childRel);
130
+ }
131
+ };
132
+ walk("");
133
+ return found;
134
+ }
135
+ function probeAgents(root, agentsRel, found) {
136
+ const single = `${agentsRel}/spec/spec.md`;
137
+ if (existsSync(join(root, single))) found.push(single);
138
+ if (agentsRel === ".agents") {
139
+ const specsDir = join(root, ".agents", "specs");
140
+ let projects;
141
+ try {
142
+ projects = readdirSync(specsDir, { withFileTypes: true });
143
+ } catch {
144
+ projects = [];
145
+ }
146
+ for (const p of projects) {
147
+ if (!p.isDirectory()) continue;
148
+ const rel = `.agents/specs/${p.name}/spec.md`;
149
+ if (existsSync(join(root, rel))) found.push(rel);
150
+ }
151
+ }
152
+ }
153
+ function parseAnchorsToml(text) {
154
+ const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text);
155
+ if (!m) return [];
156
+ const out = [];
157
+ for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim());
158
+ return out.filter((s) => s !== "");
159
+ }
160
+ function readAnchors(root) {
161
+ const file = join(root, ANCHORS_CONFIG);
162
+ if (!existsSync(file)) return [];
163
+ try {
164
+ return parseAnchorsToml(readFileSync(file, "utf8"));
165
+ } catch {
166
+ process.stderr.write(`discover-specs: ignoring unreadable ${ANCHORS_CONFIG}\n`);
167
+ return [];
168
+ }
169
+ }
170
+ function collectDescendants(root, startDir) {
171
+ const out = [startDir];
172
+ let entries;
173
+ try {
174
+ entries = readdirSync(join(root, startDir), { withFileTypes: true });
175
+ } catch {
176
+ return out;
177
+ }
178
+ for (const e of entries) {
179
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
180
+ const childRel = startDir ? `${startDir}/${e.name}` : e.name;
181
+ if (isNestedCheckout(join(root, childRel))) continue;
182
+ out.push(...collectDescendants(root, childRel));
183
+ }
184
+ return out;
185
+ }
186
+ function expandAnchor(root, pattern) {
187
+ const segs = pattern.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
188
+ let frontier = [{ dir: "" }];
189
+ for (const seg of segs) {
190
+ const next = [];
191
+ if (seg === "**") {
192
+ for (const node of frontier) for (const dir of collectDescendants(root, node.dir)) next.push({
193
+ dir,
194
+ capturedName: node.capturedName
195
+ });
196
+ frontier = next;
197
+ continue;
198
+ }
199
+ const isGlob = seg === "*" || seg === "<project>";
200
+ for (const node of frontier) if (isGlob) {
201
+ let entries;
202
+ try {
203
+ entries = readdirSync(join(root, node.dir), { withFileTypes: true });
204
+ } catch {
205
+ continue;
206
+ }
207
+ for (const e of entries) {
208
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
209
+ const childRel = node.dir ? `${node.dir}/${e.name}` : e.name;
210
+ if (isNestedCheckout(join(root, childRel))) continue;
211
+ next.push({
212
+ dir: childRel,
213
+ capturedName: seg === "<project>" ? e.name : node.capturedName
214
+ });
215
+ }
216
+ } else next.push({
217
+ dir: node.dir ? `${node.dir}/${seg}` : seg,
218
+ capturedName: node.capturedName
219
+ });
220
+ frontier = next;
221
+ }
222
+ const out = [];
223
+ for (const node of frontier) {
224
+ const rel = node.dir ? `${node.dir}/spec.md` : "spec.md";
225
+ if (existsSync(join(root, rel))) out.push({
226
+ rel,
227
+ capturedName: node.capturedName
228
+ });
229
+ }
230
+ return out;
231
+ }
232
+ function recordFor(root, rel, loc) {
233
+ let fm;
234
+ try {
235
+ fm = parseFrontmatter(readFileSync(join(root, rel), "utf8"));
236
+ } catch {
237
+ return null;
238
+ }
239
+ if (!fm?.status || !LIFECYCLE_STATUSES.has(fm.status)) return null;
240
+ const { name, nameSource } = deriveName(loc, fm);
241
+ return {
242
+ path: rel.replace(/\/spec\.md$/, ""),
243
+ name,
244
+ nameSource,
245
+ status: fm.status,
246
+ projectPath: fm.projectPath ?? "",
247
+ approvals: Object.entries(fm.approval).map(([gate, verdict]) => `${gate}:${verdict}`).join(";")
248
+ };
249
+ }
250
+ function collectSpecs(root) {
251
+ const out = [];
252
+ const seen = /* @__PURE__ */ new Set();
253
+ for (const rel of discoverSpecFiles(root)) {
254
+ const loc = classifyLocation(rel);
255
+ if (!loc) continue;
256
+ const rec = recordFor(root, rel, loc);
257
+ if (rec) {
258
+ out.push(rec);
259
+ seen.add(rel);
260
+ }
261
+ }
262
+ for (const pattern of readAnchors(root)) for (const { rel, capturedName } of expandAnchor(root, pattern)) {
263
+ if (seen.has(rel)) continue;
264
+ const rec = recordFor(root, rel, {
265
+ pattern: "extra",
266
+ locationDir: rel.replace(/\/spec\.md$/, ""),
267
+ capturedName
268
+ });
269
+ if (rec) {
270
+ out.push(rec);
271
+ seen.add(rel);
272
+ }
273
+ }
274
+ return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
275
+ }
276
+ function resolveByName(specs, name) {
277
+ const want = name.trim().toLowerCase();
278
+ const hits = specs.filter((s) => s.name.toLowerCase() === want);
279
+ if (hits.length === 0) return { kind: "none" };
280
+ if (hits.length === 1) return {
281
+ kind: "match",
282
+ spec: hits[0]
283
+ };
284
+ return {
285
+ kind: "ambiguous",
286
+ candidates: hits
287
+ };
288
+ }
289
+ const COLUMNS = [
290
+ "path",
291
+ "name",
292
+ "nameSource",
293
+ "status",
294
+ "projectPath",
295
+ "approvals"
296
+ ];
297
+ function toonField(v) {
298
+ if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
299
+ return v;
300
+ }
301
+ function toToon(specs) {
302
+ return [`specs[${specs.length}]{${COLUMNS.join(",")}}:`, ...specs.map((s) => ` ${COLUMNS.map((c) => toonField(s[c])).join(",")}`)].join("\n");
303
+ }
304
+ function main(argv) {
305
+ const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
306
+ const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
307
+ let specs = collectSpecs(root);
308
+ if (argv.includes("--resolve")) {
309
+ const name = argv[argv.indexOf("--resolve") + 1] ?? "";
310
+ const r = resolveByName(specs, name);
311
+ specs = r.kind === "match" ? [r.spec] : r.kind === "ambiguous" ? r.candidates : [];
312
+ }
313
+ const out = format === "json" ? JSON.stringify(specs, null, 2) : toToon(specs);
314
+ process.stdout.write(`${out}\n`);
315
+ return 0;
316
+ }
317
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
318
+ //#endregion
319
+ export { LIFECYCLE_STATUSES, classifyLocation, collectSpecs, deriveName, discoverSpecFiles, expandAnchor, main, parseAnchorsToml, parseFrontmatter, readAnchors, resolveByName, toToon };
@@ -0,0 +1,12 @@
1
+ // Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
2
+ // A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
3
+ // will not strip types, so it runs the self-contained bundle built for the package.
4
+ import { spawnSync } from 'node:child_process'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const self = fileURLToPath(import.meta.url)
8
+ const installed = /[\\/]node_modules[\\/]/.test(self)
9
+ const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
10
+ const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
11
+ if (run.error) throw run.error
12
+ process.exitCode = run.status ?? 1
@@ -58,11 +58,11 @@ recorded as distilled).
58
58
  **Determine distilled-ness by a structured JSONL parse, never a substring or regex text match.**
59
59
  The ledger shards are JSONL (one JSON object per line). **Reuse the existing parse contract** rather
60
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
61
+ `../plan-retirement/scripts/retire-plans.mts` (its `--distilled` flag) over the project `ledger/` directory and membership-test
62
62
  the mission's `<cr-ref>` against the set it returns, e.g.
63
63
 
64
64
  ```
65
- node -e 'import("../plan-retirement/scripts/retire-plans.mts").then(m => console.log([...m.distilledCrRefs(process.argv[1])].join("\n")))' <ledger-dir>
65
+ node "<plan-retirement skill>/scripts/retire-plans.mjs" --distilled <ledger-dir>
66
66
  ```
67
67
 
68
68
  `distilledCrRefs` parses **each line** with `JSON.parse` (keying only on `kind === "strategy"` with a
@@ -58,6 +58,16 @@ misplaced only when it **neither** follows the declared strategy **nor** matches
58
58
  — judging on the strategy alone raises false findings against placements a human already adjudicated. A declared layout settles *where a node goes*; it never licenses a capability to
59
59
  scatter across nodes. (Strategy is policy — read it; homes are data — derive them.)
60
60
 
61
+ **Depth on capability nodes; `design/` by its own shape** (`sdd:spec-structure-governance`). The
62
+ two-level cap covers capability nodes: one three levels deep is a depth finding naming it, its
63
+ sub-grouping owed to a `concept:` tag. `design/` is judged by its own shape instead — current design
64
+ at the top, one file per perspective, the `decisions/` ADR log, and at most one version folder
65
+ holding the next design. The placement map names that version folder and its cut trigger; the named
66
+ folder and its files raise no finding, any other `design/` sub-folder besides `decisions/` is a
67
+ perspective-folder finding, a named version with no cut trigger is a finding, and so is a named
68
+ version whose folder does not exist (a stale record the cut left behind). Tell the two
69
+ apart by the map, never by the folder's contents.
70
+
61
71
  ## The intra-spec structural acts
62
72
 
63
73
  It acts on each spec's **structure**, not its content — one project is **one spec**, so structural
@@ -91,10 +101,7 @@ shares one boundary.
91
101
  axis is wrong), not a granularity split to carve as-proposed.
92
102
 
93
103
  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.)
104
+ big" into "too big **along which axis** — and is that axis real?"
98
105
 
99
106
  Alongside its findings a pass surfaces an **advisory layout-quality signal** — the scheduler's
100
107
  **false-conflict rate** doubles as a **partition-quality metric**: a layout that keeps node↔folder
@@ -14,7 +14,7 @@ conductor's autonomy bar (baked into `start-mission`), a hard floor + a three-di
14
14
  ## Legal-state tuples
15
15
 
16
16
  The mechanical authority is `spec-gate/scripts/check-spec-state.mts` — run it
17
- (`node <skill>/scripts/check-spec-state.mts`) to enforce; if `node` is unavailable, apply the same
17
+ (`node <skill>/scripts/check-spec-state.mjs`) to enforce; if `node` is unavailable, apply the same
18
18
  rules by reading frontmatter. The `(status, markers, .feature, approval)` tuple of the root
19
19
  `spec.md` — plus the durable gate lines globbed from its sibling `ledger/` shards (a legacy `ledger.jsonl` still counts) — is **illegal** when:
20
20
 
@@ -11,9 +11,9 @@ and invokes no gate.
11
11
  - **Tests:** [`scripts/wire-statusline.test.mts`](./scripts/wire-statusline.test.mts) (`node:test`)
12
12
 
13
13
  ```bash
14
- node scripts/wire-statusline.mts --root . --detect
15
- node scripts/wire-statusline.mts --root . --wire --mode own-line
16
- node scripts/wire-statusline.mts --root . --wire --mode same-line # --no-global-base to shadow deliberately
14
+ node scripts/wire-statusline.mjs --root . --detect
15
+ node scripts/wire-statusline.mjs --root . --wire --mode own-line
16
+ node scripts/wire-statusline.mjs --root . --wire --mode same-line # --no-global-base to shadow deliberately
17
17
  ```
18
18
 
19
19
  The engine writes only project `.claude/settings.json` (never the global settings file) and, in a
@@ -29,7 +29,7 @@ On enable, ask whether the mission status should render:
29
29
  Before wiring, run the read-only detection — Claude Code's project `statusLine` **replaces** the global one (no merge), so a fresh project wiring would silently shadow a status line living in `~/.claude/settings.json`:
30
30
 
31
31
  ```bash
32
- node "<skill>/scripts/wire-statusline.mts" --root . --detect
32
+ node "<skill>/scripts/wire-statusline.mjs" --root . --detect
33
33
  ```
34
34
 
35
35
  It reports the project statusLine (absent | wired | foreign), the global statusLine (present | absent), and the shadow risk. On a shadow risk (project absent, global present), tell the user wiring will shadow their global status line and ask:
@@ -44,8 +44,8 @@ No shadow risk → proceed straight to wiring.
44
44
  Run the engine to compose the reader into project settings and (in a git repo) gitignore the status file:
45
45
 
46
46
  ```bash
47
- node "<skill>/scripts/wire-statusline.mts" --root . --wire --mode own-line
48
- node "<skill>/scripts/wire-statusline.mts" --root . --wire --mode same-line # add --no-global-base to shadow deliberately
47
+ node "<skill>/scripts/wire-statusline.mjs" --root . --wire --mode own-line
48
+ node "<skill>/scripts/wire-statusline.mjs" --root . --wire --mode same-line # add --no-global-base to shadow deliberately
49
49
  ```
50
50
 
51
51
  The engine:
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ //#region skills/init/scripts/wire-statusline.mts
7
+ const STATUS_FILE = ".agents/sdd/statusline";
8
+ const SETTINGS_FILE = ".claude/settings.json";
9
+ const GITIGNORE_FILE = ".gitignore";
10
+ const GLOBAL_SETTINGS_FILE = join(homedir(), ".claude", "settings.json");
11
+ const MARKER_BEGIN = "# sdd-statusline:begin";
12
+ const MARKER_END = "# sdd-statusline:end";
13
+ const MODE_PREFIX = "# sdd-statusline:mode:";
14
+ const ORIG_OPEN = "__sdd_orig() {";
15
+ const ORIG_CLOSE = "}";
16
+ const NO_BASE = ":";
17
+ function buildCommand(base, mode) {
18
+ const origBody = base && base.trim() !== "" ? base : NO_BASE;
19
+ const render = mode === "own-line" ? `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s\\n%s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi` : `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s | %s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi`;
20
+ return [
21
+ MARKER_BEGIN,
22
+ `${MODE_PREFIX}${mode}`,
23
+ ORIG_OPEN,
24
+ origBody,
25
+ ORIG_CLOSE,
26
+ "__sdd_base=\"$(__sdd_orig 2>/dev/null)\"",
27
+ `__sdd_file="${STATUS_FILE}"`,
28
+ render,
29
+ MARKER_END
30
+ ].join("\n");
31
+ }
32
+ function parseCommand(command) {
33
+ if (!command?.includes(MARKER_BEGIN)) return { wired: false };
34
+ const modeMatch = new RegExp(`${MODE_PREFIX}(own-line|same-line)`).exec(command);
35
+ const origMatch = /__sdd_orig\(\) \{\n([\s\S]*?)\n\}/.exec(command);
36
+ const mode = modeMatch?.[1] ?? "own-line";
37
+ const origBody = origMatch?.[1] ?? NO_BASE;
38
+ return {
39
+ wired: true,
40
+ base: origBody === NO_BASE ? void 0 : origBody,
41
+ mode
42
+ };
43
+ }
44
+ function baseToWrap(existingCommand) {
45
+ const parsed = parseCommand(existingCommand);
46
+ return parsed.wired ? parsed.base : existingCommand;
47
+ }
48
+ function readSettingsFile(file) {
49
+ if (!existsSync(file)) return {};
50
+ try {
51
+ return JSON.parse(readFileSync(file, "utf8"));
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+ function readSettings(root) {
57
+ return readSettingsFile(join(root, SETTINGS_FILE));
58
+ }
59
+ function readStatusLineCommand(file) {
60
+ const command = readSettingsFile(file).statusLine?.command;
61
+ if (typeof command !== "string" || command.trim() === "") return void 0;
62
+ return command;
63
+ }
64
+ function writeSettings(root, settings) {
65
+ const file = join(root, SETTINGS_FILE);
66
+ mkdirSync(dirname(file), { recursive: true });
67
+ writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
68
+ }
69
+ function composeStatusLine(root, mode, globalCommand) {
70
+ const settings = readSettings(root);
71
+ const existing = settings.statusLine?.command;
72
+ const hasProjectStatusLine = typeof existing === "string" && existing.trim() !== "";
73
+ const globalBaseComposed = !hasProjectStatusLine && globalCommand !== void 0;
74
+ const command = buildCommand(hasProjectStatusLine ? baseToWrap(existing) : globalCommand, mode);
75
+ if (existing === command) return {
76
+ changed: false,
77
+ command,
78
+ globalBaseComposed: false
79
+ };
80
+ settings.statusLine = {
81
+ ...settings.statusLine,
82
+ type: "command",
83
+ command
84
+ };
85
+ writeSettings(root, settings);
86
+ return {
87
+ changed: true,
88
+ command,
89
+ globalBaseComposed
90
+ };
91
+ }
92
+ function isGitRepo(root) {
93
+ return existsSync(join(root, ".git"));
94
+ }
95
+ function splitLines(text) {
96
+ const lines = text.split("\n");
97
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
98
+ return lines;
99
+ }
100
+ function addGitignoreEntry(root) {
101
+ const file = join(root, GITIGNORE_FILE);
102
+ const lines = existsSync(file) ? splitLines(readFileSync(file, "utf8")) : [];
103
+ if (lines.some((l) => l.trim() === ".agents/sdd/statusline")) return { changed: false };
104
+ const next = [...lines, STATUS_FILE];
105
+ writeFileSync(file, `${next.join("\n")}\n`);
106
+ return { changed: true };
107
+ }
108
+ function detectStatusLines(root, globalSettingsFile) {
109
+ const projectCommand = readStatusLineCommand(join(root, SETTINGS_FILE));
110
+ const project = projectCommand === void 0 ? "absent" : parseCommand(projectCommand).wired ? "wired" : "foreign";
111
+ const global = readStatusLineCommand(globalSettingsFile ?? GLOBAL_SETTINGS_FILE) !== void 0 ? "present" : "absent";
112
+ return {
113
+ project,
114
+ global,
115
+ shadow: project === "absent" && global === "present"
116
+ };
117
+ }
118
+ function wireStatusline(root, mode, opts) {
119
+ return {
120
+ settings: composeStatusLine(root, mode, opts?.globalBase ?? true ? readStatusLineCommand(opts?.globalSettingsFile ?? GLOBAL_SETTINGS_FILE) : void 0),
121
+ gitignore: isGitRepo(root) ? {
122
+ ...addGitignoreEntry(root),
123
+ skipped: false
124
+ } : {
125
+ changed: false,
126
+ skipped: true
127
+ }
128
+ };
129
+ }
130
+ function flag(argv, name) {
131
+ const i = argv.indexOf(name);
132
+ return i === -1 ? void 0 : argv[i + 1];
133
+ }
134
+ function main(argv) {
135
+ const root = flag(argv, "--root") ?? ".";
136
+ const globalSettingsFile = flag(argv, "--global-settings");
137
+ const w = (s) => process.stdout.write(`${s}\n`);
138
+ if (argv.includes("--detect")) {
139
+ const result = detectStatusLines(root, globalSettingsFile);
140
+ w(`project statusLine: ${result.project}`);
141
+ w(`global statusLine: ${result.global}`);
142
+ w(result.shadow ? "shadow risk: yes — wiring would shadow the global statusline; compose it as the base (default) or pass --no-global-base to shadow deliberately" : "shadow risk: no");
143
+ return 0;
144
+ }
145
+ if (argv.includes("--wire")) {
146
+ const mode = flag(argv, "--mode");
147
+ if (mode !== "own-line" && mode !== "same-line") {
148
+ w("refused: --mode must be own-line or same-line");
149
+ return 1;
150
+ }
151
+ const result = wireStatusline(root, mode, {
152
+ globalSettingsFile,
153
+ globalBase: !argv.includes("--no-global-base")
154
+ });
155
+ w(result.settings.changed ? `wired statusLine (${mode})` : "statusLine already wired (unchanged)");
156
+ if (result.settings.globalBaseComposed) w("composed the global statusLine command as the wrapped base");
157
+ if (result.gitignore.skipped) w("not a git repo — gitignore skipped");
158
+ else w(result.gitignore.changed ? `added ${STATUS_FILE} to .gitignore` : ".gitignore already up to date");
159
+ return 0;
160
+ }
161
+ w("usage: wire-statusline --root <dir> --wire --mode own-line|same-line [--global-settings <file>] [--no-global-base]\n wire-statusline --root <dir> --detect [--global-settings <file>]");
162
+ return 0;
163
+ }
164
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
165
+ //#endregion
166
+ export { GITIGNORE_FILE, GLOBAL_SETTINGS_FILE, SETTINGS_FILE, STATUS_FILE, addGitignoreEntry, buildCommand, composeStatusLine, detectStatusLines, isGitRepo, main, parseCommand, readStatusLineCommand, wireStatusline };
@@ -0,0 +1,12 @@
1
+ // Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
2
+ // A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
3
+ // will not strip types, so it runs the self-contained bundle built for the package.
4
+ import { spawnSync } from 'node:child_process'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const self = fileURLToPath(import.meta.url)
8
+ const installed = /[\\/]node_modules[\\/]/.test(self)
9
+ const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
10
+ const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
11
+ if (run.error) throw run.error
12
+ process.exitCode = run.status ?? 1
@@ -11,9 +11,9 @@ file. Order is meaningful (last-match-wins), so rules are never re-sorted.
11
11
  - **Tests:** [`scripts/manage-ignore.test.mts`](./scripts/manage-ignore.test.mts) (`node:test`)
12
12
 
13
13
  ```bash
14
- node scripts/manage-ignore.mts --list
15
- node scripts/manage-ignore.mts --induce build/output/app.log
16
- node scripts/manage-ignore.mts --preview '*.log'
17
- node scripts/manage-ignore.mts --preview '!keep.log'
18
- node scripts/manage-ignore.mts --add '*.log'
14
+ node scripts/manage-ignore.mjs --list
15
+ node scripts/manage-ignore.mjs --induce build/output/app.log
16
+ node scripts/manage-ignore.mjs --preview '*.log'
17
+ node scripts/manage-ignore.mjs --preview '!keep.log'
18
+ node scripts/manage-ignore.mjs --add '*.log'
19
19
  ```
@@ -25,7 +25,7 @@ engine therefore **never re-sorts**: `--add` appends, `--remove` preserves the o
25
25
  ## Run an operation
26
26
 
27
27
  ```bash
28
- node "<skill>/scripts/manage-ignore.mts" [--root .] <operation>
28
+ node "<skill>/scripts/manage-ignore.mjs" [--root .] <operation>
29
29
  ```
30
30
 
31
31
  | Operation | Effect |