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,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
@@ -572,8 +572,10 @@ function readStdin(): string {
572
572
  }
573
573
  }
574
574
 
575
- const HERE = dirname(fileURLToPath(import.meta.url))
576
- const DISCOVER_SPECS_PATH = join(HERE, '..', '..', 'discover-specs', 'scripts', 'discover-specs.mts')
575
+ const HERE = fileURLToPath(import.meta.url)
576
+ // A bundle (built for npm, where Node will not strip types) runs its sibling engines as bundles too.
577
+ const ENGINE_EXT = HERE.endsWith('.mts') ? '.mts' : '.bundle.mjs'
578
+ const DISCOVER_SPECS_PATH = join(dirname(HERE), '..', '..', 'discover-specs', 'scripts', `discover-specs${ENGINE_EXT}`)
577
579
 
578
580
  interface DiscoveredSpec {
579
581
  path: string
@@ -92,7 +92,7 @@ field.
92
92
  ### `report` — per-subagent dispatch (combat log)
93
93
 
94
94
  ```jsonl
95
- {"seq": 3, "ts": "2026-06-28T18:30:11Z", "handle": "unional", "kind": "report", "role": "spec-producer", "agent": "sdd:automaton", "outcome": "pass", "summary": "wrote 14 scenarios covering the ledger expansion"}
95
+ {"seq": 3, "ts": "2026-06-28T18:30:11Z", "handle": "unional", "kind": "report", "role": "spec-producer", "agent": "sdd:sdd-automaton", "outcome": "pass", "summary": "wrote 14 scenarios covering the ledger expansion"}
96
96
  ```
97
97
 
98
98
  `role` is the production role dispatched; `agent` is the plugin-qualified agent name; `outcome` is
@@ -5,8 +5,8 @@ every node's `concept:` frontmatter and renders the **by-concept view** into the
5
5
  re-unifying a cross-cutting concern the capability folder tree scatters.
6
6
 
7
7
  ```bash
8
- node scripts/concept-index.mts --spec-dir <spec> --write # refresh the block
9
- node scripts/concept-index.mts --spec-dir <spec> --check # no-drift guard
8
+ node scripts/concept-index.mjs --spec-dir <spec> --write # refresh the block
9
+ node scripts/concept-index.mjs --spec-dir <spec> --check # no-drift guard
10
10
  ```
11
11
 
12
12
  Pure derivation, frontmatter only; the write touches only the delimited generated block. See
@@ -17,7 +17,7 @@ carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps conven
17
17
  ## Run it
18
18
 
19
19
  ```bash
20
- node "<skill>/scripts/concept-index.mts" --spec-dir <spec> [--write | --check]
20
+ node "<skill>/scripts/concept-index.mjs" --spec-dir <spec> [--write | --check]
21
21
  ```
22
22
 
23
23
  - default (no mode) — print the rendered "By concept" section to stdout (dry run).
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ //#region skills/concept-index/scripts/concept-index.mts
6
+ const BEGIN_MARKER = "<!-- BEGIN generated: by-concept (project-spec/concept-index) -->";
7
+ const END_MARKER = "<!-- END generated: by-concept -->";
8
+ const ANCHOR_HEADING = "## Invariants";
9
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
10
+ "node_modules",
11
+ ".git",
12
+ "dist",
13
+ ".turbo",
14
+ ".next",
15
+ "coverage"
16
+ ]);
17
+ function parseFrontmatter(text) {
18
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
19
+ if (!m) return null;
20
+ const fm = {
21
+ concepts: [],
22
+ model: false
23
+ };
24
+ const lines = m[1].split("\n").map((l) => l.replace(/\r$/, ""));
25
+ for (let i = 0; i < lines.length; i++) {
26
+ const line = lines[i];
27
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
28
+ if (line.length - line.trimStart().length !== 0) continue;
29
+ const [key, ...rest] = line.trim().split(":");
30
+ const value = rest.join(":").trim();
31
+ if (key === "spec-type") fm.specType = unquote(value);
32
+ else if (key === "model") fm.model = unquote(value) === "true";
33
+ else if (key === "concept") {
34
+ if (value === "" || value === "|" || value === ">") for (let j = i + 1; j < lines.length; j++) {
35
+ const item = lines[j];
36
+ if (item.trim() === "") continue;
37
+ if (item.length - item.trimStart().length === 0) break;
38
+ const dash = /^\s*-\s+(.*)$/.exec(item);
39
+ if (dash) fm.concepts.push(unquote(dash[1].trim()));
40
+ }
41
+ else fm.concepts.push(...parseScalarOrFlow(value));
42
+ }
43
+ }
44
+ return fm;
45
+ }
46
+ function parseScalarOrFlow(value) {
47
+ const v = value.trim();
48
+ if (v.startsWith("[") && v.endsWith("]")) return v.slice(1, -1).split(",").map((s) => unquote(s.trim())).filter((s) => s.length > 0);
49
+ const single = unquote(v);
50
+ return single.length > 0 ? [single] : [];
51
+ }
52
+ function unquote(v) {
53
+ return v.replace(/^["']|["']$/g, "");
54
+ }
55
+ function facetKind(relPath, specType) {
56
+ const p = relPath.replace(/\\/g, "/");
57
+ if (p.startsWith("design/")) return "rule";
58
+ if (p.startsWith("workflows/")) return "workflow";
59
+ if (specType === "reference") return "reference";
60
+ if (specType === "behavioral") return "behavior";
61
+ return "index";
62
+ }
63
+ function displayPath(relPath) {
64
+ const p = relPath.replace(/\\/g, "/");
65
+ return p.endsWith("/README.md") ? p.slice(0, -9) : p;
66
+ }
67
+ function scanProjectSpec(specDir) {
68
+ const records = [];
69
+ walk(specDir, specDir, records);
70
+ return records;
71
+ }
72
+ function walk(dir, specDir, out) {
73
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
74
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
75
+ const full = join(dir, entry.name);
76
+ if (entry.isDirectory()) walk(full, specDir, out);
77
+ else if (entry.name.endsWith(".md")) {
78
+ const fm = parseFrontmatter(readFileSync(full, "utf8"));
79
+ if (!fm || fm.concepts.length === 0) continue;
80
+ const relPath = relative(specDir, full).replace(/\\/g, "/");
81
+ out.push({
82
+ relPath,
83
+ display: displayPath(relPath),
84
+ concepts: fm.concepts,
85
+ facet: facetKind(relPath, fm.specType)
86
+ });
87
+ }
88
+ }
89
+ }
90
+ function groupByConcept(records) {
91
+ const grouped = /* @__PURE__ */ new Map();
92
+ for (const rec of records) for (const concept of rec.concepts) {
93
+ const list = grouped.get(concept) ?? [];
94
+ list.push(rec);
95
+ grouped.set(concept, list);
96
+ }
97
+ return new Map([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, [...v].sort((x, y) => x.display.localeCompare(y.display))]));
98
+ }
99
+ function renderTable(grouped) {
100
+ return [
101
+ "| Concept | Facets |",
102
+ "|---|---|",
103
+ ...[...grouped.entries()].map(([concept, nodes]) => {
104
+ return `| \`${concept}\` | ${nodes.map((n) => `\`${n.display}\` (${n.facet})`).join(" · ")} |`;
105
+ })
106
+ ].join("\n");
107
+ }
108
+ function renderSection(grouped) {
109
+ return [
110
+ BEGIN_MARKER,
111
+ "",
112
+ "## By concept",
113
+ "",
114
+ "> Generated from `concept:` frontmatter by `project-spec/concept-index` — do not edit by hand.",
115
+ "",
116
+ renderTable(grouped),
117
+ "",
118
+ END_MARKER
119
+ ].join("\n");
120
+ }
121
+ function applySection(specText, section) {
122
+ const begin = specText.indexOf(BEGIN_MARKER);
123
+ const end = specText.indexOf(END_MARKER);
124
+ if (begin !== -1 && end !== -1 && end > begin) {
125
+ const before = specText.slice(0, begin);
126
+ const after = specText.slice(end + 34);
127
+ return before + section + after;
128
+ }
129
+ const anchor = specText.indexOf(ANCHOR_HEADING);
130
+ if (anchor !== -1) return specText.slice(0, anchor) + section + "\n\n" + specText.slice(anchor);
131
+ return `${specText.replace(/\s*$/, "")}\n\n${section}\n`;
132
+ }
133
+ function extractSection(specText) {
134
+ const begin = specText.indexOf(BEGIN_MARKER);
135
+ const end = specText.indexOf(END_MARKER);
136
+ if (begin === -1 || end === -1 || end < begin) return null;
137
+ return specText.slice(begin, end + 34);
138
+ }
139
+ function parseArgs(argv) {
140
+ let specDir = ".";
141
+ let mode = "print";
142
+ for (let i = 0; i < argv.length; i++) {
143
+ const a = argv[i];
144
+ if (a === "--spec-dir") specDir = argv[++i] ?? ".";
145
+ else if (a === "--write") mode = "write";
146
+ else if (a === "--check") mode = "check";
147
+ }
148
+ return {
149
+ specDir,
150
+ mode
151
+ };
152
+ }
153
+ function main(argv) {
154
+ const { specDir, mode } = parseArgs(argv);
155
+ const section = renderSection(groupByConcept(scanProjectSpec(specDir)));
156
+ const specPath = join(specDir, "spec.md");
157
+ if (mode === "print") {
158
+ process.stdout.write(`${section}\n`);
159
+ return 0;
160
+ }
161
+ if (!existsSync(specPath)) {
162
+ process.stderr.write(`concept-index: no spec.md at ${specPath}\n`);
163
+ return 2;
164
+ }
165
+ const specText = readFileSync(specPath, "utf8");
166
+ if (mode === "check") {
167
+ if (extractSection(specText) === section) {
168
+ process.stdout.write("concept-index: no drift\n");
169
+ return 0;
170
+ }
171
+ process.stderr.write("concept-index: drift — spec.md by-concept block is stale; run --write\n");
172
+ return 1;
173
+ }
174
+ const next = applySection(specText, section);
175
+ if (next !== specText) writeFileSync(specPath, next);
176
+ process.stdout.write(`concept-index: ${next === specText ? "unchanged" : "updated"} ${specPath}\n`);
177
+ return 0;
178
+ }
179
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
180
+ //#endregion
181
+ export { BEGIN_MARKER, END_MARKER, applySection, extractSection, facetKind, groupByConcept, main, parseFrontmatter, parseScalarOrFlow, renderSection, renderTable, scanProjectSpec };
@@ -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,6 +11,6 @@ an unretired/resumable mission, tallies its todos, reads its top-level `status`
11
11
  - **Tests:** [`scripts/discover-plans.test.mts`](./scripts/discover-plans.test.mts) (`node:test`)
12
12
 
13
13
  ```bash
14
- node scripts/discover-plans.mts --root . --format toon
15
- node scripts/discover-plans.mts --root . --status approved # the dispatch queue: approved briefs only
14
+ node scripts/discover-plans.mjs --root . --format toon
15
+ node scripts/discover-plans.mjs --root . --status approved # the dispatch queue: approved briefs only
16
16
  ```
@@ -42,7 +42,7 @@ the flag; it never interprets or writes it.
42
42
  ## Run the scan
43
43
 
44
44
  ```bash
45
- node "<skill>/scripts/discover-plans.mts" [--root .] [--format toon|json] [--status <value>]
45
+ node "<skill>/scripts/discover-plans.mjs" [--root .] [--format toon|json] [--status <value>]
46
46
  ```
47
47
 
48
48
  - Default `--root` is the current directory; default `--format` is **TOON** (the token-efficient
@@ -0,0 +1,135 @@
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-plans/scripts/discover-plans.mts
6
+ const TODO_STATUSES = /* @__PURE__ */ new Set([
7
+ "pending",
8
+ "in_progress",
9
+ "completed"
10
+ ]);
11
+ /** The default mission dispatch status — the meaning of an absent top-level `status`. */
12
+ const DEFAULT_PLAN_STATUS = "active";
13
+ function parsePlanFrontmatter(text) {
14
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
15
+ if (!m) return null;
16
+ const fm = {
17
+ name: "",
18
+ status: DEFAULT_PLAN_STATUS,
19
+ total: 0,
20
+ completed: 0,
21
+ inProgress: 0,
22
+ pending: 0
23
+ };
24
+ let inTodos = false;
25
+ for (const raw of m[1].split("\n")) {
26
+ const line = raw.replace(/\r$/, "");
27
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
28
+ const indent = line.length - line.trimStart().length;
29
+ const trimmed = line.trim();
30
+ if (indent === 0) {
31
+ inTodos = false;
32
+ const [key, ...rest] = trimmed.split(":");
33
+ const value = rest.join(":").trim();
34
+ if (key === "name") fm.name = unquote(value);
35
+ else if (key === "status") {
36
+ const v = unquote(value);
37
+ if (v !== "") fm.status = v;
38
+ } else if (key === "todos") inTodos = true;
39
+ continue;
40
+ }
41
+ if (inTodos) {
42
+ const sm = /^(?:-\s+)?status:\s*(.+)$/.exec(trimmed);
43
+ if (sm) {
44
+ const status = unquote(sm[1].trim());
45
+ fm.total++;
46
+ if (status === "completed") fm.completed++;
47
+ else if (status === "in_progress") fm.inProgress++;
48
+ else if (status === "pending") fm.pending++;
49
+ }
50
+ }
51
+ }
52
+ return fm;
53
+ }
54
+ function unquote(v) {
55
+ return v.replace(/^["']|["']$/g, "");
56
+ }
57
+ function nextLead(text) {
58
+ const lines = text.split("\n");
59
+ let i = lines.findIndex((l) => /^##\s+NEXT\b/i.test(l.trim()));
60
+ if (i === -1) return "";
61
+ for (i += 1; i < lines.length; i++) {
62
+ const l = lines[i].replace(/\r$/, "").trim();
63
+ if (l === "") continue;
64
+ if (l.startsWith("#")) return "";
65
+ const cleaned = l.replace(/^[-*]\s+/, "").replace(/\*\*/g, "").trim();
66
+ return cleaned.length > 200 ? `${cleaned.slice(0, 197)}...` : cleaned;
67
+ }
68
+ return "";
69
+ }
70
+ function discoverPlanFiles(root) {
71
+ const dir = join(root, ".agents", "plans");
72
+ if (!existsSync(dir)) return [];
73
+ let entries;
74
+ try {
75
+ entries = readdirSync(dir, { withFileTypes: true });
76
+ } catch {
77
+ return [];
78
+ }
79
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".plan.md")).map((e) => e.name).sort();
80
+ }
81
+ function collectPlans(root) {
82
+ const out = [];
83
+ for (const name of discoverPlanFiles(root)) {
84
+ let text;
85
+ try {
86
+ text = readFileSync(join(root, ".agents", "plans", name), "utf8");
87
+ } catch {
88
+ continue;
89
+ }
90
+ const fm = parsePlanFrontmatter(text);
91
+ if (!fm) continue;
92
+ out.push({
93
+ cr: name.replace(/\.plan\.md$/, ""),
94
+ name: fm.name,
95
+ total: fm.total,
96
+ completed: fm.completed,
97
+ inProgress: fm.inProgress,
98
+ status: fm.status,
99
+ next: nextLead(text)
100
+ });
101
+ }
102
+ return out.sort((a, b) => a.cr < b.cr ? -1 : a.cr > b.cr ? 1 : 0);
103
+ }
104
+ function filterByStatus(plans, status) {
105
+ return plans.filter((p) => p.status === status);
106
+ }
107
+ const COLUMNS = [
108
+ "cr",
109
+ "name",
110
+ "total",
111
+ "completed",
112
+ "inProgress",
113
+ "status",
114
+ "next"
115
+ ];
116
+ function toonField(v) {
117
+ if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
118
+ return v;
119
+ }
120
+ function toToon(plans) {
121
+ return [`plans[${plans.length}]{${COLUMNS.join(",")}}:`, ...plans.map((p) => ` ${COLUMNS.map((c) => toonField(String(p[c]))).join(",")}`)].join("\n");
122
+ }
123
+ function main(argv) {
124
+ const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
125
+ const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
126
+ const statusFilter = argv.includes("--status") ? argv[argv.indexOf("--status") + 1] : void 0;
127
+ let plans = collectPlans(root);
128
+ if (statusFilter !== void 0) plans = filterByStatus(plans, statusFilter);
129
+ const out = format === "json" ? JSON.stringify(plans, null, 2) : toToon(plans);
130
+ process.stdout.write(`${out}\n`);
131
+ return 0;
132
+ }
133
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
134
+ //#endregion
135
+ export { DEFAULT_PLAN_STATUS, TODO_STATUSES, collectPlans, filterByStatus, main, nextLead, parsePlanFrontmatter, 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
@@ -11,5 +11,5 @@ specs found.
11
11
  - **Tests:** [`scripts/discover-specs.test.mts`](./scripts/discover-specs.test.mts) (`node:test`)
12
12
 
13
13
  ```bash
14
- node scripts/discover-specs.mts --root . --format toon
14
+ node scripts/discover-specs.mjs --root . --format toon
15
15
  ```
@@ -37,7 +37,7 @@ A `spec.md` is a spec only when **both** hold (ADR-0017, narrowed; extra anchors
37
37
  ## Run the scan
38
38
 
39
39
  ```bash
40
- node "<skill>/scripts/discover-specs.mts" [--root .] [--format toon|json] [--resolve <name>]
40
+ node "<skill>/scripts/discover-specs.mjs" [--root .] [--format toon|json] [--resolve <name>]
41
41
  ```
42
42
 
43
43
  - Default `--root` is the current directory; default `--format` is **TOON** (the token-efficient