cyber-sdd 0.7.0 → 0.7.1

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 (149) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/agents/sdd-scanner.md +1 -1
  4. package/package.json +11 -13
  5. package/plugin.json +55 -0
  6. package/skills/align-spec/SKILL.md +1 -1
  7. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  8. package/skills/blast-estimate/README.md +3 -3
  9. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  10. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  11. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  12. package/skills/check-plan-safety/SKILL.md +2 -3
  13. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  14. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  15. package/skills/check-project-specs/SKILL.md +1 -1
  16. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  17. package/skills/check-retired-terms/SKILL.md +2 -3
  18. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  19. package/skills/check-scenario-overlap/SKILL.md +1 -2
  20. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  21. package/skills/check-spec-references/SKILL.md +2 -2
  22. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  23. package/skills/check-spec-structure/SKILL.md +1 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  25. package/skills/collision-ladder/README.md +2 -2
  26. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  27. package/skills/combat-log-governance/SKILL.md +1 -1
  28. package/skills/concept-index/SKILL.md +1 -1
  29. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  30. package/skills/discover-plans/README.md +3 -3
  31. package/skills/discover-plans/SKILL.md +1 -1
  32. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  33. package/skills/discover-specs/README.md +3 -3
  34. package/skills/discover-specs/SKILL.md +1 -1
  35. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  36. package/skills/doctrine-loop/SKILL.md +1 -1
  37. package/skills/gate-validation-governance/SKILL.md +1 -1
  38. package/skills/init/README.md +2 -2
  39. package/skills/init/scripts/wire-statusline.mjs +168 -12
  40. package/skills/manage-ignore/README.md +3 -3
  41. package/skills/manage-ignore/SKILL.md +1 -2
  42. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  43. package/skills/manage-scenario-bridge/README.md +3 -3
  44. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  45. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  46. package/skills/manage-spec-anchors/README.md +3 -3
  47. package/skills/manage-spec-anchors/SKILL.md +1 -1
  48. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  49. package/skills/mission-graph/README.md +2 -2
  50. package/skills/mission-graph/SKILL.md +1 -1
  51. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  52. package/skills/pause-mission/SKILL.md +1 -1
  53. package/skills/place-node/SKILL.md +2 -2
  54. package/skills/place-node/scripts/place-node.mjs +117 -12
  55. package/skills/plan-retirement/README.md +2 -2
  56. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  57. package/skills/resolve-governances/README.md +3 -3
  58. package/skills/resolve-governances/SKILL.md +1 -1
  59. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  60. package/skills/resolve-tracking/SKILL.md +1 -1
  61. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  62. package/skills/scaffold-project-spec/SKILL.md +1 -1
  63. package/skills/spec-gate/README.md +12 -9
  64. package/skills/spec-gate/SKILL.md +2 -2
  65. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  66. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  67. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  68. package/skills/spec-producer-governance/SKILL.md +2 -2
  69. package/skills/start-mission/SKILL.md +1 -1
  70. package/skills/suite-format-governance/README.md +1 -1
  71. package/skills/suite-format-governance/SKILL.md +1 -1
  72. package/skills/touch-set-correction/README.md +5 -5
  73. package/skills/touch-set-correction/SKILL.md +1 -1
  74. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  75. package/skills/verify-scenarios/README.md +6 -6
  76. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  77. package/.plugin/pins.json +0 -3
  78. package/.plugin/plugin.json +0 -19
  79. package/dist/align-spec/scripts/align-spec.js +0 -102
  80. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  81. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  82. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  83. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  84. package/dist/concept-index/scripts/concept-index.js +0 -181
  85. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  86. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  87. package/dist/spec-gate/scripts/check-suite.js +0 -317
  88. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  89. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  90. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  91. package/skills/align-spec/scripts/align-spec.mts +0 -188
  92. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  93. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  94. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  95. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  96. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  97. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  98. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  99. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  100. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  102. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  103. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  104. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  105. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  106. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  108. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  109. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  110. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  111. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  112. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  113. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  114. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  115. package/skills/concept-index/scripts/concept-index.mts +0 -246
  116. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  117. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  118. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  119. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  120. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  121. package/skills/init/scripts/wire-statusline.mts +0 -279
  122. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  123. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  124. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  126. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  128. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  129. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  130. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  131. package/skills/place-node/scripts/place-node.mts +0 -158
  132. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  133. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  134. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  135. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  136. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  137. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  138. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  139. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  140. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  141. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  142. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  143. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  144. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  145. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  146. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  147. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  148. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  149. 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
- }