cyber-sdd 0.7.0 → 0.7.2

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 (150) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/CHANGELOG.md +320 -0
  4. package/agents/sdd-scanner.md +1 -1
  5. package/package.json +11 -12
  6. package/plugin.json +55 -0
  7. package/skills/align-spec/SKILL.md +1 -1
  8. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  9. package/skills/blast-estimate/README.md +3 -3
  10. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  11. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  12. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  13. package/skills/check-plan-safety/SKILL.md +2 -3
  14. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  15. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  16. package/skills/check-project-specs/SKILL.md +1 -1
  17. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  18. package/skills/check-retired-terms/SKILL.md +2 -3
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  20. package/skills/check-scenario-overlap/SKILL.md +1 -2
  21. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  22. package/skills/check-spec-references/SKILL.md +2 -2
  23. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  24. package/skills/check-spec-structure/SKILL.md +1 -2
  25. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  26. package/skills/collision-ladder/README.md +2 -2
  27. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  28. package/skills/combat-log-governance/SKILL.md +1 -1
  29. package/skills/concept-index/SKILL.md +1 -1
  30. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  31. package/skills/discover-plans/README.md +3 -3
  32. package/skills/discover-plans/SKILL.md +1 -1
  33. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  34. package/skills/discover-specs/README.md +3 -3
  35. package/skills/discover-specs/SKILL.md +1 -1
  36. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  37. package/skills/doctrine-loop/SKILL.md +1 -1
  38. package/skills/gate-validation-governance/SKILL.md +1 -1
  39. package/skills/init/README.md +2 -2
  40. package/skills/init/scripts/wire-statusline.mjs +168 -12
  41. package/skills/manage-ignore/README.md +3 -3
  42. package/skills/manage-ignore/SKILL.md +1 -2
  43. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  44. package/skills/manage-scenario-bridge/README.md +3 -3
  45. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  46. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  47. package/skills/manage-spec-anchors/README.md +3 -3
  48. package/skills/manage-spec-anchors/SKILL.md +1 -1
  49. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  50. package/skills/mission-graph/README.md +2 -2
  51. package/skills/mission-graph/SKILL.md +1 -1
  52. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  53. package/skills/pause-mission/SKILL.md +1 -1
  54. package/skills/place-node/SKILL.md +2 -2
  55. package/skills/place-node/scripts/place-node.mjs +117 -12
  56. package/skills/plan-retirement/README.md +2 -2
  57. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  58. package/skills/resolve-governances/README.md +3 -3
  59. package/skills/resolve-governances/SKILL.md +1 -1
  60. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  61. package/skills/resolve-tracking/SKILL.md +1 -1
  62. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  63. package/skills/scaffold-project-spec/SKILL.md +1 -1
  64. package/skills/spec-gate/README.md +12 -9
  65. package/skills/spec-gate/SKILL.md +2 -2
  66. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  67. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  68. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  69. package/skills/spec-producer-governance/SKILL.md +2 -2
  70. package/skills/start-mission/SKILL.md +1 -1
  71. package/skills/suite-format-governance/README.md +1 -1
  72. package/skills/suite-format-governance/SKILL.md +1 -1
  73. package/skills/touch-set-correction/README.md +5 -5
  74. package/skills/touch-set-correction/SKILL.md +1 -1
  75. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  76. package/skills/verify-scenarios/README.md +6 -6
  77. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  78. package/.plugin/pins.json +0 -3
  79. package/.plugin/plugin.json +0 -19
  80. package/dist/align-spec/scripts/align-spec.js +0 -102
  81. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  82. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  83. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  84. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  85. package/dist/concept-index/scripts/concept-index.js +0 -181
  86. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  87. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  88. package/dist/spec-gate/scripts/check-suite.js +0 -317
  89. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  90. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  91. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  92. package/skills/align-spec/scripts/align-spec.mts +0 -188
  93. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  94. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  95. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  96. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  97. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  98. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  99. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  100. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  102. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  103. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  104. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  105. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  106. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  108. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  109. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  110. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  111. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  112. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  113. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  114. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  115. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  116. package/skills/concept-index/scripts/concept-index.mts +0 -246
  117. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  118. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  119. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  120. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  121. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  124. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  126. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  128. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  129. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  130. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  131. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  132. package/skills/place-node/scripts/place-node.mts +0 -158
  133. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  134. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  135. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  136. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  137. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  138. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  139. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  140. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  141. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  142. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  143. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  144. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  145. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  146. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  147. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  148. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  149. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  150. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,115 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFileSync, readdirSync, realpathSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- //#region skills/place-node/scripts/place-node.mts
6
- const SKIP_DIRS = /* @__PURE__ */ new Set([
7
- "node_modules",
8
- ".git",
9
- "dist",
10
- ".turbo",
11
- ".next",
12
- "coverage"
13
- ]);
14
- function parseConcepts(text) {
15
- const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
16
- if (!m) return [];
17
- const lines = m[1].split("\n").map((l) => l.replace(/\r$/, ""));
18
- for (let i = 0; i < lines.length; i++) {
19
- const line = lines[i];
20
- if (line.length - line.trimStart().length !== 0) continue;
21
- const [key, ...rest] = line.trim().split(":");
22
- if (key !== "concept") continue;
23
- const value = rest.join(":").trim();
24
- if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1).split(",").map((s) => unquote(s.trim())).filter((s) => s.length > 0);
25
- if (value !== "") return [unquote(value)];
26
- const out = [];
27
- for (let j = i + 1; j < lines.length; j++) {
28
- const item = lines[j];
29
- if (item.trim() === "") continue;
30
- if (item.length - item.trimStart().length === 0) break;
31
- const dash = /^\s*-\s+(.*)$/.exec(item);
32
- if (dash) out.push(unquote(dash[1].trim()));
33
- }
34
- return out;
35
- }
36
- return [];
37
- }
38
- function unquote(v) {
39
- return v.replace(/^["']|["']$/g, "");
40
- }
41
- function displayPath(relPath) {
42
- const p = relPath.replace(/\\/g, "/");
43
- return p.endsWith("/README.md") ? p.slice(0, -9) : p;
44
- }
45
- function scanProjectSpec(specDir) {
46
- const records = [];
47
- walk(specDir, specDir, records);
48
- return records;
49
- }
50
- function walk(dir, specDir, out) {
51
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
52
- if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
53
- const full = join(dir, entry.name);
54
- if (entry.isDirectory()) walk(full, specDir, out);
55
- else if (entry.name.endsWith(".md")) {
56
- const concepts = parseConcepts(readFileSync(full, "utf8"));
57
- if (concepts.length === 0) continue;
58
- const relPath = full.slice(specDir.length + 1).replace(/\\/g, "/");
59
- out.push({
60
- relPath,
61
- capability: relPath.split("/")[0],
62
- display: displayPath(relPath),
63
- concepts
64
- });
65
- }
66
- }
67
- }
68
- function suggestHomes(records, concept) {
69
- const counts = /* @__PURE__ */ new Map();
70
- for (const rec of records) {
71
- if (!rec.concepts.includes(concept)) continue;
72
- counts.set(rec.capability, (counts.get(rec.capability) ?? 0) + 1);
73
- }
74
- return [...counts.entries()].map(([capability, count]) => ({
75
- capability,
76
- count
77
- })).sort((a, b) => b.count - a.count || a.capability.localeCompare(b.capability));
78
- }
79
- function findNear(records, name) {
80
- const needle = name.trim().toLowerCase();
81
- if (needle === "") return [];
82
- return records.filter((rec) => rec.relPath.toLowerCase().includes(needle)).sort((a, b) => a.display.localeCompare(b.display));
83
- }
84
- function render(homes, near, concept, name) {
85
- const lines = [`place-node: concept=${concept || "(none)"} name=${name || "(none)"}`];
86
- if (homes.length > 0) {
87
- lines.push(`homes[${homes.length}]{capability,facets}:`);
88
- for (const h of homes) lines.push(` ${h.capability},${h.count}`);
89
- } else lines.push("homes[0]: (new concept — pick any plausible capability; finalized at handoff)");
90
- if (near.length > 0) {
91
- lines.push(`near[${near.length}]:`);
92
- for (const n of near) lines.push(` ${n.display}`);
93
- } else lines.push("near[0]: (no name overlap)");
94
- lines.push("note: placement is provisional — finalized at handoff");
95
- return lines.join("\n");
96
- }
97
- function main(argv) {
98
- let specDir = ".";
99
- let concept = "";
100
- let name = "";
101
- for (let i = 0; i < argv.length; i++) {
102
- const a = argv[i];
103
- if (a === "--spec-dir") specDir = argv[++i] ?? ".";
104
- else if (a === "--concept") concept = argv[++i] ?? "";
105
- else if (a === "--name") name = argv[++i] ?? "";
106
- }
107
- const records = scanProjectSpec(specDir);
108
- const homes = concept ? suggestHomes(records, concept) : [];
109
- const near = name ? findNear(records, name) : [];
110
- process.stdout.write(`${render(homes, near, concept, name)}\n`);
111
- return 0;
112
- }
113
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
114
- //#endregion
115
- export { findNear, main, parseConcepts, scanProjectSpec, suggestHomes };
@@ -1,158 +0,0 @@
1
- #!/usr/bin/env node
2
- // place-node — project-spec/place-node's concrete engine. Given a new node's concept (and optional name),
3
- // suggests a provisional capability home and surfaces possible duplicates, so explore places a node in
4
- // one lookup instead of holding the tree in its head.
5
- //
6
- // Derivation, not a registry: the home for a concept is where that concept's facets already sit — read
7
- // live from the project-spec's `concept:` tags, never a stored routing list (the corpus/discovery no-drift
8
- // rule). Read-only and advisory: it writes nothing, and placement is finalized at handoff.
9
- //
10
- // No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions are exported for
11
- // node:test; running the file directly drives the CLI.
12
-
13
- import { readdirSync, readFileSync, realpathSync } from 'node:fs'
14
- import { join } from 'node:path'
15
- import { pathToFileURL } from 'node:url'
16
-
17
- const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
18
-
19
- export interface NodeRecord {
20
- /** Path relative to the spec directory (POSIX). */
21
- relPath: string
22
- /** The top-level folder — the capability. */
23
- capability: string
24
- /** Display form — a README.md node shows its folder, a design doc shows the file. */
25
- display: string
26
- concepts: string[]
27
- }
28
-
29
- export interface HomeSuggestion {
30
- capability: string
31
- count: number
32
- }
33
-
34
- // ── Frontmatter parse (concept only — the placement signal) ──
35
- export function parseConcepts(text: string): string[] {
36
- const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text)
37
- if (!m) return []
38
- const lines = m[1].split('\n').map((l) => l.replace(/\r$/, ''))
39
- for (let i = 0; i < lines.length; i++) {
40
- const line = lines[i]
41
- if (line.length - line.trimStart().length !== 0) continue
42
- const [key, ...rest] = line.trim().split(':')
43
- if (key !== 'concept') continue
44
- const value = rest.join(':').trim()
45
- if (value.startsWith('[') && value.endsWith(']')) {
46
- return value
47
- .slice(1, -1)
48
- .split(',')
49
- .map((s) => unquote(s.trim()))
50
- .filter((s) => s.length > 0)
51
- }
52
- if (value !== '') return [unquote(value)]
53
- // block list
54
- const out: string[] = []
55
- for (let j = i + 1; j < lines.length; j++) {
56
- const item = lines[j]
57
- if (item.trim() === '') continue
58
- if (item.length - item.trimStart().length === 0) break
59
- const dash = /^\s*-\s+(.*)$/.exec(item)
60
- if (dash) out.push(unquote(dash[1].trim()))
61
- }
62
- return out
63
- }
64
- return []
65
- }
66
-
67
- function unquote(v: string): string {
68
- return v.replace(/^["']|["']$/g, '')
69
- }
70
-
71
- function displayPath(relPath: string): string {
72
- const p = relPath.replace(/\\/g, '/')
73
- return p.endsWith('/README.md') ? p.slice(0, -'README.md'.length) : p
74
- }
75
-
76
- // ── Scan the project-spec — every concept-tagged node ──
77
- export function scanProjectSpec(specDir: string): NodeRecord[] {
78
- const records: NodeRecord[] = []
79
- walk(specDir, specDir, records)
80
- return records
81
- }
82
-
83
- function walk(dir: string, specDir: string, out: NodeRecord[]): void {
84
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
85
- if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue
86
- const full = join(dir, entry.name)
87
- if (entry.isDirectory()) {
88
- walk(full, specDir, out)
89
- } else if (entry.name.endsWith('.md')) {
90
- const concepts = parseConcepts(readFileSync(full, 'utf8'))
91
- if (concepts.length === 0) continue
92
- const relPath = full.slice(specDir.length + 1).replace(/\\/g, '/')
93
- out.push({ relPath, capability: relPath.split('/')[0], display: displayPath(relPath), concepts })
94
- }
95
- }
96
- }
97
-
98
- // ── Suggest a home — where the concept's facets already sit, ranked by count ──
99
- export function suggestHomes(records: NodeRecord[], concept: string): HomeSuggestion[] {
100
- const counts = new Map<string, number>()
101
- for (const rec of records) {
102
- if (!rec.concepts.includes(concept)) continue
103
- counts.set(rec.capability, (counts.get(rec.capability) ?? 0) + 1)
104
- }
105
- return [...counts.entries()]
106
- .map(([capability, count]) => ({ capability, count }))
107
- .sort((a, b) => b.count - a.count || a.capability.localeCompare(b.capability))
108
- }
109
-
110
- // ── Duplicate-catch — existing nodes whose path/name overlaps the candidate name ──
111
- export function findNear(records: NodeRecord[], name: string): NodeRecord[] {
112
- const needle = name.trim().toLowerCase()
113
- if (needle === '') return []
114
- return records
115
- .filter((rec) => rec.relPath.toLowerCase().includes(needle))
116
- .sort((a, b) => a.display.localeCompare(b.display))
117
- }
118
-
119
- // ── Render ──
120
- function render(homes: HomeSuggestion[], near: NodeRecord[], concept: string, name: string): string {
121
- const lines: string[] = [`place-node: concept=${concept || '(none)'} name=${name || '(none)'}`]
122
- if (homes.length > 0) {
123
- lines.push(`homes[${homes.length}]{capability,facets}:`)
124
- for (const h of homes) lines.push(` ${h.capability},${h.count}`)
125
- } else {
126
- lines.push('homes[0]: (new concept — pick any plausible capability; finalized at handoff)')
127
- }
128
- if (near.length > 0) {
129
- lines.push(`near[${near.length}]:`)
130
- for (const n of near) lines.push(` ${n.display}`)
131
- } else {
132
- lines.push('near[0]: (no name overlap)')
133
- }
134
- lines.push('note: placement is provisional — finalized at handoff')
135
- return lines.join('\n')
136
- }
137
-
138
- // ── CLI ──
139
- export function main(argv: string[]): number {
140
- let specDir = '.'
141
- let concept = ''
142
- let name = ''
143
- for (let i = 0; i < argv.length; i++) {
144
- const a = argv[i]
145
- if (a === '--spec-dir') specDir = argv[++i] ?? '.'
146
- else if (a === '--concept') concept = argv[++i] ?? ''
147
- else if (a === '--name') name = argv[++i] ?? ''
148
- }
149
- const records = scanProjectSpec(specDir)
150
- const homes = concept ? suggestHomes(records, concept) : []
151
- const near = name ? findNear(records, name) : []
152
- process.stdout.write(`${render(homes, near, concept, name)}\n`)
153
- return 0
154
- }
155
-
156
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
157
- process.exit(main(process.argv.slice(2)))
158
- }
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, readFileSync, readdirSync, realpathSync, unlinkSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- //#region skills/plan-retirement/scripts/retire-plans.mts
6
- const PLAN_SUFFIX = ".plan.md";
7
- const LOG_SUFFIX = ".log.jsonl";
8
- const DESIGN_SUFFIX = ".design.md";
9
- const OPERATIONS_SUFFIX = ".operations.md";
10
- const EVIDENCE_SUFFIX = ".evidence.md";
11
- function transientArtifactFiles(crRef) {
12
- return [
13
- `${crRef}${PLAN_SUFFIX}`,
14
- `${crRef}${LOG_SUFFIX}`,
15
- `${crRef}${DESIGN_SUFFIX}`,
16
- `${crRef}${OPERATIONS_SUFFIX}`,
17
- `${crRef}${EVIDENCE_SUFFIX}`
18
- ];
19
- }
20
- function parseCleared(value) {
21
- if (!value) return [];
22
- const seen = /* @__PURE__ */ new Set();
23
- for (const raw of value.split(",")) {
24
- const ref = raw.trim();
25
- if (ref) seen.add(ref);
26
- }
27
- return [...seen];
28
- }
29
- function discoverPlans(root) {
30
- let entries;
31
- try {
32
- entries = readdirSync(root);
33
- } catch {
34
- return [];
35
- }
36
- return entries.filter((f) => f.endsWith(PLAN_SUFFIX)).map((f) => f.slice(0, -8));
37
- }
38
- function discoverLogs(root) {
39
- let entries;
40
- try {
41
- entries = readdirSync(root);
42
- } catch {
43
- return [];
44
- }
45
- return entries.filter((f) => f.endsWith(LOG_SUFFIX)).map((f) => f.slice(0, -10));
46
- }
47
- function distilledCrRefs(ledgerDir) {
48
- const distilled = /* @__PURE__ */ new Set();
49
- let entries;
50
- try {
51
- entries = readdirSync(ledgerDir);
52
- } catch {
53
- return distilled;
54
- }
55
- for (const entry of entries) {
56
- if (!entry.endsWith(".jsonl")) continue;
57
- let text;
58
- try {
59
- text = readFileSync(join(ledgerDir, entry), "utf8");
60
- } catch {
61
- continue;
62
- }
63
- for (const line of text.split("\n")) {
64
- const trimmed = line.trim();
65
- if (!trimmed) continue;
66
- let record;
67
- try {
68
- record = JSON.parse(trimmed);
69
- } catch {
70
- continue;
71
- }
72
- if (record === null || typeof record !== "object") continue;
73
- const { kind, distills } = record;
74
- if (kind === "strategy" && typeof distills === "string" && distills.length > 0) distilled.add(distills);
75
- }
76
- }
77
- return distilled;
78
- }
79
- function decideRetirements(cleared, existing, distilled, logPresent) {
80
- const present = new Set(existing);
81
- const seen = /* @__PURE__ */ new Set();
82
- const out = [];
83
- for (const ref of cleared) if (present.has(ref) && (distilled.has(ref) || !logPresent.has(ref)) && !seen.has(ref)) {
84
- seen.add(ref);
85
- out.push(ref);
86
- }
87
- return out;
88
- }
89
- function main(argv) {
90
- const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] : ".agents/plans";
91
- const ledgerArg = argv.includes("--ledger") ? argv[argv.indexOf("--ledger") + 1] : void 0;
92
- const cleared = parseCleared(argv.includes("--retire") ? argv[argv.indexOf("--retire") + 1] : void 0);
93
- const dryRun = argv.includes("--dry-run");
94
- if (argv.includes("--distilled")) {
95
- for (const ref of distilledCrRefs(argv[argv.indexOf("--distilled") + 1] ?? "")) process.stdout.write(`${ref}\n`);
96
- return 0;
97
- }
98
- let retiring;
99
- if (!ledgerArg || !existsSync(ledgerArg)) {
100
- process.stdout.write(`no verifiable ledger (${ledgerArg ? `--ledger ${ledgerArg} unreadable` : "--ledger not given"}): distillation cannot be checked, so retirement is skipped for all cr-refs\n`);
101
- retiring = [];
102
- } else {
103
- const distilled = distilledCrRefs(ledgerArg);
104
- const logPresent = new Set(discoverLogs(root));
105
- retiring = decideRetirements(cleared, discoverPlans(root), distilled, logPresent);
106
- }
107
- const deleted = [];
108
- for (const ref of retiring) for (const file of transientArtifactFiles(ref)) {
109
- const path = join(root, file);
110
- if (!existsSync(path)) continue;
111
- if (!dryRun) unlinkSync(path);
112
- deleted.push(path);
113
- }
114
- const verb = dryRun ? "would delete" : "deleted";
115
- for (const path of deleted) process.stdout.write(`${verb} ${path}\n`);
116
- process.stdout.write(`${dryRun ? "dry-run: " : ""}retired ${retiring.length} plan(s), ${deleted.length} file(s)\n`);
117
- return 0;
118
- }
119
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
120
- //#endregion
121
- export { decideRetirements, discoverLogs, discoverPlans, distilledCrRefs, main, parseCleared, transientArtifactFiles };
@@ -1,205 +0,0 @@
1
- #!/usr/bin/env node
2
- // Plan retirement — the Doctrine loop's last retro step (see sdd:combat-log-governance,
3
- // "Plan retirement"). A retired plan leaves the tree by a deliberate, gated TRACKED
4
- // DELETION, never a gitignore side effect: for each cleared <cr-ref>, the sweep deletes that
5
- // CR's whole TRANSIENT ARTIFACT SET — the plan pair (<cr-ref>.plan.md + <cr-ref>.log.jsonl)
6
- // plus its transient CR-level planning briefs (<cr-ref>.design.md, <cr-ref>.operations.md,
7
- // <cr-ref>.evidence.md) — from `.agents/plans`.
8
- //
9
- // The two gating signals split by verifiability. Source = done/merged stays the CALLER's
10
- // judgment: the source-status query (github-NN -> GH issue, asana-<gid> -> Asana,
11
- // local-<slug> -> the local store) needs network/gh, so the Scanner (doctrine-loop delegate)
12
- // determines it and passes the CLEARED set via --retire. Distilled is local and mechanically
13
- // checkable, so the sweep VERIFIES IT ITSELF against the project ledger (--ledger <dir>): a
14
- // `strategy` entry whose `distills` field equals the <cr-ref> must exist. The distilled gate
15
- // only guards a cr-ref whose combat log EXISTS — there is something on disk it could still
16
- // distill from, so retirement without a distilling entry would be data loss. A cr-ref whose
17
- // <cr-ref>.log.jsonl was never written has nothing to distill from in the first place, so it
18
- // retires on clearance + presence alone, no distilling entry required. This is the
19
- // mechanical, fail-closed gate + filesystem act:
20
- // - it deletes the transient artifact set for a cr-ref that is cleared AND present on disk
21
- // AND (distilled per the ledger OR has no log.jsonl to distill from);
22
- // - a missing/unreadable --ledger skips retirement for ALL cr-refs (the no-log branch only
23
- // applies once a ledger is actually present to consult);
24
- // - anything not cleared, not present, or already gone is a no-op (idempotent, safe to
25
- // re-run);
26
- // - it never touches a plan it was not explicitly cleared to retire (fail-closed).
27
- //
28
- // Two invariants on the transient briefs (design.md / operations.md / evidence.md), both
29
- // deliberate:
30
- // - THEY DO NOT WIDEN THE DISTILLED GATE. The gate keys on the combat log's presence only
31
- // (discoverLogs / logPresent) — a cr-ref with briefs but no log.jsonl still retires
32
- // without a distilling strategy. A brief owes no distillation (its content was consumed
33
- // by the mission itself, not extracted into the ledger), so gating on one would re-strand
34
- // the no-log mission class the no-log branch exists to rescue.
35
- // - THEY DO NOT ANCHOR PRESENCE. discoverPlans (keyed on .plan.md) stays the sole presence
36
- // signal. A cr-ref with a design.md but no plan.md is a no-op — the idempotency contract
37
- // (a cleared cr-ref with no plan on disk deletes nothing) is the stronger guarantee.
38
- //
39
- // Pure functions are exported for node:test; running the file directly drives the CLI.
40
- // No dependencies. Use --dry-run to print the planned deletions without touching the tree.
41
-
42
- import { existsSync, readdirSync, readFileSync, realpathSync, unlinkSync } from 'node:fs'
43
- import { join } from 'node:path'
44
- import { pathToFileURL } from 'node:url'
45
-
46
- const PLAN_SUFFIX = '.plan.md'
47
- const LOG_SUFFIX = '.log.jsonl'
48
- const DESIGN_SUFFIX = '.design.md'
49
- const OPERATIONS_SUFFIX = '.operations.md'
50
- const EVIDENCE_SUFFIX = '.evidence.md'
51
-
52
- // The full transient artifact set a retired cr-ref owns, in deletion order: the plan pair
53
- // (plan.md, log.jsonl) then the optional transient CR-level planning briefs (design.md,
54
- // operations.md, evidence.md). The briefs are optional — the per-file existsSync guard in
55
- // main() no-ops any that are absent, which is what lets a cr-ref with only some briefs retire
56
- // cleanly.
57
- export function transientArtifactFiles(crRef: string): string[] {
58
- return [
59
- `${crRef}${PLAN_SUFFIX}`,
60
- `${crRef}${LOG_SUFFIX}`,
61
- `${crRef}${DESIGN_SUFFIX}`,
62
- `${crRef}${OPERATIONS_SUFFIX}`,
63
- `${crRef}${EVIDENCE_SUFFIX}`,
64
- ]
65
- }
66
-
67
- // Parse a --retire value (comma-separated cr-refs) into a clean, de-duplicated list.
68
- export function parseCleared(value: string | undefined): string[] {
69
- if (!value) return []
70
- const seen = new Set<string>()
71
- for (const raw of value.split(',')) {
72
- const ref = raw.trim()
73
- if (ref) seen.add(ref)
74
- }
75
- return [...seen]
76
- }
77
-
78
- // The cr-refs that have a <cr-ref>.plan.md on disk under `root`.
79
- export function discoverPlans(root: string): string[] {
80
- let entries: string[]
81
- try {
82
- entries = readdirSync(root)
83
- } catch {
84
- return []
85
- }
86
- return entries.filter((f) => f.endsWith(PLAN_SUFFIX)).map((f) => f.slice(0, -PLAN_SUFFIX.length))
87
- }
88
-
89
- // The cr-refs that have a <cr-ref>.log.jsonl on disk under `root`.
90
- export function discoverLogs(root: string): string[] {
91
- let entries: string[]
92
- try {
93
- entries = readdirSync(root)
94
- } catch {
95
- return []
96
- }
97
- return entries.filter((f) => f.endsWith(LOG_SUFFIX)).map((f) => f.slice(0, -LOG_SUFFIX.length))
98
- }
99
-
100
- // The cr-refs the project ledger records as DISTILLED — a `strategy` entry whose `distills`
101
- // field equals the cr-ref exists in some *.jsonl shard under `ledgerDir`. Keys on the
102
- // structured `distills` field only: a cr-ref named merely inside a strategy's `evidence`
103
- // array (a cross-reference) does NOT count, and an unratified entry (`ratified: false`, the
104
- // Scanner's default) still counts — the gate is about what was distilled, not sign-off.
105
- // Malformed lines and a missing/unreadable ledger dir are tolerated: fail-closed means an
106
- // unverifiable ledger yields an EMPTY set, never a thrown error.
107
- export function distilledCrRefs(ledgerDir: string): Set<string> {
108
- const distilled = new Set<string>()
109
- let entries: string[]
110
- try {
111
- entries = readdirSync(ledgerDir)
112
- } catch {
113
- return distilled
114
- }
115
- for (const entry of entries) {
116
- if (!entry.endsWith('.jsonl')) continue
117
- let text: string
118
- try {
119
- text = readFileSync(join(ledgerDir, entry), 'utf8')
120
- } catch {
121
- continue
122
- }
123
- for (const line of text.split('\n')) {
124
- const trimmed = line.trim()
125
- if (!trimmed) continue
126
- let record: unknown
127
- try {
128
- record = JSON.parse(trimmed)
129
- } catch {
130
- continue
131
- }
132
- if (record === null || typeof record !== 'object') continue
133
- const { kind, distills } = record as { kind?: unknown; distills?: unknown }
134
- if (kind === 'strategy' && typeof distills === 'string' && distills.length > 0) distilled.add(distills)
135
- }
136
- }
137
- return distilled
138
- }
139
-
140
- // Fail-closed, idempotent decision: retire exactly the cr-refs that are cleared by the caller,
141
- // present on disk, AND either distilled per the ledger OR have no combat log to distill from
142
- // (log.jsonl absent). An uncleared or absent plan is never retired; a present plan whose log
143
- // DOES exist still requires a distilling entry (data-loss guard, unchanged). Order follows the
144
- // cleared list for stable reporting.
145
- export function decideRetirements(
146
- cleared: string[],
147
- existing: string[],
148
- distilled: Set<string>,
149
- logPresent: Set<string>,
150
- ): string[] {
151
- const present = new Set(existing)
152
- const seen = new Set<string>()
153
- const out: string[] = []
154
- for (const ref of cleared) {
155
- if (present.has(ref) && (distilled.has(ref) || !logPresent.has(ref)) && !seen.has(ref)) {
156
- seen.add(ref)
157
- out.push(ref)
158
- }
159
- }
160
- return out
161
- }
162
-
163
- export function main(argv: string[]): number {
164
- const root = argv.includes('--root') ? argv[argv.indexOf('--root') + 1] : '.agents/plans'
165
- const ledgerArg = argv.includes('--ledger') ? argv[argv.indexOf('--ledger') + 1] : undefined
166
- const cleared = parseCleared(argv.includes('--retire') ? argv[argv.indexOf('--retire') + 1] : undefined)
167
- const dryRun = argv.includes('--dry-run')
168
-
169
- // `--distilled <ledger-dir>`: list the cr-refs the ledger records as distilled, one per line.
170
- if (argv.includes('--distilled')) {
171
- for (const ref of distilledCrRefs(argv[argv.indexOf('--distilled') + 1] ?? '')) process.stdout.write(`${ref}\n`)
172
- return 0
173
- }
174
-
175
- let retiring: string[]
176
- if (!ledgerArg || !existsSync(ledgerArg)) {
177
- process.stdout.write(
178
- `no verifiable ledger (${ledgerArg ? `--ledger ${ledgerArg} unreadable` : '--ledger not given'}): distillation cannot be checked, so retirement is skipped for all cr-refs\n`,
179
- )
180
- retiring = []
181
- } else {
182
- const distilled = distilledCrRefs(ledgerArg)
183
- const logPresent = new Set(discoverLogs(root))
184
- retiring = decideRetirements(cleared, discoverPlans(root), distilled, logPresent)
185
- }
186
- const deleted: string[] = []
187
-
188
- for (const ref of retiring) {
189
- for (const file of transientArtifactFiles(ref)) {
190
- const path = join(root, file)
191
- if (!existsSync(path)) continue // the log may be absent; delete what is there
192
- if (!dryRun) unlinkSync(path)
193
- deleted.push(path)
194
- }
195
- }
196
-
197
- const verb = dryRun ? 'would delete' : 'deleted'
198
- for (const path of deleted) process.stdout.write(`${verb} ${path}\n`)
199
- process.stdout.write(`${dryRun ? 'dry-run: ' : ''}retired ${retiring.length} plan(s), ${deleted.length} file(s)\n`)
200
- return 0
201
- }
202
-
203
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
204
- process.exit(main(process.argv.slice(2)))
205
- }