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.
- package/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -242,7 +242,7 @@ plan/log-surfaced candidate against **current code** (a persisted plan or log is
|
|
|
242
242
|
closed candidate.
|
|
243
243
|
|
|
244
244
|
```jsonl
|
|
245
|
-
{"seq": 3, "handle": "sdd-scanner", "kind": "strategy", "disposition": "resolved", "recommendation": "no action — coverage-gap mechanism already shipped", "evidence": ["current-code: checkReferencedArtifacts + checkUseCaseCoverage live in spec-gate/scripts/check-spec-state.
|
|
245
|
+
{"seq": 3, "handle": "sdd-scanner", "kind": "strategy", "disposition": "resolved", "recommendation": "no action — coverage-gap mechanism already shipped", "evidence": ["current-code: checkReferencedArtifacts + checkUseCaseCoverage live in spec-gate/scripts/check-spec-state.mjs"], "ratified": false}
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
`disposition` is set **once at write** and never flipped (the append-only invariant) — the Scanner's
|
|
@@ -12,7 +12,7 @@ The concrete engine for the **concept-index** step. It scans
|
|
|
12
12
|
one project-spec for every node's `concept:` frontmatter and renders the **by-concept view** —
|
|
13
13
|
`concept → {its nodes across every folder}` — that re-unifies a cross-cutting concern the capability
|
|
14
14
|
folder tree scatters (the concept axis: one concern enacted across several capability folders). It
|
|
15
|
-
carries a self-contained `.
|
|
15
|
+
carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11).
|
|
16
16
|
|
|
17
17
|
## Run it
|
|
18
18
|
|
|
@@ -1,12 +1,183 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/concept-index/concept-index.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.2","entry":"skills/concept-index/scripts/concept-index.mjs","commands":{}};
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
//#region src/concept-index/concept-index.mts
|
|
8
|
+
const BEGIN_MARKER = "<!-- BEGIN generated: by-concept (project-spec/concept-index) -->";
|
|
9
|
+
const END_MARKER = "<!-- END generated: by-concept -->";
|
|
10
|
+
const ANCHOR_HEADING = "## Invariants";
|
|
11
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
12
|
+
"node_modules",
|
|
13
|
+
".git",
|
|
14
|
+
"dist",
|
|
15
|
+
".turbo",
|
|
16
|
+
".next",
|
|
17
|
+
"coverage"
|
|
18
|
+
]);
|
|
19
|
+
function parseFrontmatter(text) {
|
|
20
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
21
|
+
if (!m) return null;
|
|
22
|
+
const fm = {
|
|
23
|
+
concepts: [],
|
|
24
|
+
model: false
|
|
25
|
+
};
|
|
26
|
+
const lines = m[1].split("\n").map((l) => l.replace(/\r$/, ""));
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
30
|
+
if (line.length - line.trimStart().length !== 0) continue;
|
|
31
|
+
const [key, ...rest] = line.trim().split(":");
|
|
32
|
+
const value = rest.join(":").trim();
|
|
33
|
+
if (key === "spec-type") fm.specType = unquote(value);
|
|
34
|
+
else if (key === "model") fm.model = unquote(value) === "true";
|
|
35
|
+
else if (key === "concept") {
|
|
36
|
+
if (value === "" || value === "|" || value === ">") for (let j = i + 1; j < lines.length; j++) {
|
|
37
|
+
const item = lines[j];
|
|
38
|
+
if (item.trim() === "") continue;
|
|
39
|
+
if (item.length - item.trimStart().length === 0) break;
|
|
40
|
+
const dash = /^\s*-\s+(.*)$/.exec(item);
|
|
41
|
+
if (dash) fm.concepts.push(unquote(dash[1].trim()));
|
|
42
|
+
}
|
|
43
|
+
else fm.concepts.push(...parseScalarOrFlow(value));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return fm;
|
|
47
|
+
}
|
|
48
|
+
function parseScalarOrFlow(value) {
|
|
49
|
+
const v = value.trim();
|
|
50
|
+
if (v.startsWith("[") && v.endsWith("]")) return v.slice(1, -1).split(",").map((s) => unquote(s.trim())).filter((s) => s.length > 0);
|
|
51
|
+
const single = unquote(v);
|
|
52
|
+
return single.length > 0 ? [single] : [];
|
|
53
|
+
}
|
|
54
|
+
function unquote(v) {
|
|
55
|
+
return v.replace(/^["']|["']$/g, "");
|
|
56
|
+
}
|
|
57
|
+
function facetKind(relPath, specType) {
|
|
58
|
+
const p = relPath.replace(/\\/g, "/");
|
|
59
|
+
if (p.startsWith("design/")) return "rule";
|
|
60
|
+
if (p.startsWith("workflows/")) return "workflow";
|
|
61
|
+
if (specType === "reference") return "reference";
|
|
62
|
+
if (specType === "behavioral") return "behavior";
|
|
63
|
+
return "index";
|
|
64
|
+
}
|
|
65
|
+
function displayPath(relPath) {
|
|
66
|
+
const p = relPath.replace(/\\/g, "/");
|
|
67
|
+
return p.endsWith("/README.md") ? p.slice(0, -9) : p;
|
|
68
|
+
}
|
|
69
|
+
function scanProjectSpec(specDir) {
|
|
70
|
+
const records = [];
|
|
71
|
+
walk(specDir, specDir, records);
|
|
72
|
+
return records;
|
|
73
|
+
}
|
|
74
|
+
function walk(dir, specDir, out) {
|
|
75
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
76
|
+
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
77
|
+
const full = join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) walk(full, specDir, out);
|
|
79
|
+
else if (entry.name.endsWith(".md")) {
|
|
80
|
+
const fm = parseFrontmatter(readFileSync(full, "utf8"));
|
|
81
|
+
if (!fm || fm.concepts.length === 0) continue;
|
|
82
|
+
const relPath = relative(specDir, full).replace(/\\/g, "/");
|
|
83
|
+
out.push({
|
|
84
|
+
relPath,
|
|
85
|
+
display: displayPath(relPath),
|
|
86
|
+
concepts: fm.concepts,
|
|
87
|
+
facet: facetKind(relPath, fm.specType)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function groupByConcept(records) {
|
|
93
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const rec of records) for (const concept of rec.concepts) {
|
|
95
|
+
const list = grouped.get(concept) ?? [];
|
|
96
|
+
list.push(rec);
|
|
97
|
+
grouped.set(concept, list);
|
|
98
|
+
}
|
|
99
|
+
return new Map([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, [...v].sort((x, y) => x.display.localeCompare(y.display))]));
|
|
100
|
+
}
|
|
101
|
+
function renderTable(grouped) {
|
|
102
|
+
return [
|
|
103
|
+
"| Concept | Facets |",
|
|
104
|
+
"|---|---|",
|
|
105
|
+
...[...grouped.entries()].map(([concept, nodes]) => {
|
|
106
|
+
return `| \`${concept}\` | ${nodes.map((n) => `\`${n.display}\` (${n.facet})`).join(" · ")} |`;
|
|
107
|
+
})
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
function renderSection(grouped) {
|
|
111
|
+
return [
|
|
112
|
+
BEGIN_MARKER,
|
|
113
|
+
"",
|
|
114
|
+
"## By concept",
|
|
115
|
+
"",
|
|
116
|
+
"> Generated from `concept:` frontmatter by `project-spec/concept-index` — do not edit by hand.",
|
|
117
|
+
"",
|
|
118
|
+
renderTable(grouped),
|
|
119
|
+
"",
|
|
120
|
+
END_MARKER
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
|
123
|
+
function applySection(specText, section) {
|
|
124
|
+
const begin = specText.indexOf(BEGIN_MARKER);
|
|
125
|
+
const end = specText.indexOf(END_MARKER);
|
|
126
|
+
if (begin !== -1 && end !== -1 && end > begin) {
|
|
127
|
+
const before = specText.slice(0, begin);
|
|
128
|
+
const after = specText.slice(end + 34);
|
|
129
|
+
return before + section + after;
|
|
130
|
+
}
|
|
131
|
+
const anchor = specText.indexOf(ANCHOR_HEADING);
|
|
132
|
+
if (anchor !== -1) return specText.slice(0, anchor) + section + "\n\n" + specText.slice(anchor);
|
|
133
|
+
return `${specText.replace(/\s*$/, "")}\n\n${section}\n`;
|
|
134
|
+
}
|
|
135
|
+
function extractSection(specText) {
|
|
136
|
+
const begin = specText.indexOf(BEGIN_MARKER);
|
|
137
|
+
const end = specText.indexOf(END_MARKER);
|
|
138
|
+
if (begin === -1 || end === -1 || end < begin) return null;
|
|
139
|
+
return specText.slice(begin, end + 34);
|
|
140
|
+
}
|
|
141
|
+
function parseArgs(argv) {
|
|
142
|
+
let specDir = ".";
|
|
143
|
+
let mode = "print";
|
|
144
|
+
for (let i = 0; i < argv.length; i++) {
|
|
145
|
+
const a = argv[i];
|
|
146
|
+
if (a === "--spec-dir") specDir = argv[++i] ?? ".";
|
|
147
|
+
else if (a === "--write") mode = "write";
|
|
148
|
+
else if (a === "--check") mode = "check";
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
specDir,
|
|
152
|
+
mode
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function main(argv) {
|
|
156
|
+
const { specDir, mode } = parseArgs(argv);
|
|
157
|
+
const section = renderSection(groupByConcept(scanProjectSpec(specDir)));
|
|
158
|
+
const specPath = join(specDir, "spec.md");
|
|
159
|
+
if (mode === "print") {
|
|
160
|
+
process.stdout.write(`${section}\n`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
if (!existsSync(specPath)) {
|
|
164
|
+
process.stderr.write(`concept-index: no spec.md at ${specPath}\n`);
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
const specText = readFileSync(specPath, "utf8");
|
|
168
|
+
if (mode === "check") {
|
|
169
|
+
if (extractSection(specText) === section) {
|
|
170
|
+
process.stdout.write("concept-index: no drift\n");
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
process.stderr.write("concept-index: drift — spec.md by-concept block is stale; run --write\n");
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const next = applySection(specText, section);
|
|
177
|
+
if (next !== specText) writeFileSync(specPath, next);
|
|
178
|
+
process.stdout.write(`concept-index: ${next === specText ? "unchanged" : "updated"} ${specPath}\n`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
182
|
+
//#endregion
|
|
183
|
+
export { BEGIN_MARKER, END_MARKER, applySection, extractSection, facetKind, groupByConcept, main, parseFrontmatter, parseScalarOrFlow, renderSection, renderTable, scanProjectSpec };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# discover-plans
|
|
2
2
|
|
|
3
3
|
The concrete engine for SDD **plan discovery**. A non-user-invocable skill carrying a self-contained
|
|
4
|
-
`.
|
|
4
|
+
`.mjs` script that scans `.agents/plans` for `*.plan.md` mission briefs, treats each present brief as
|
|
5
5
|
an unretired/resumable mission, tallies its todos, reads its top-level `status` dispatch flag
|
|
6
6
|
(`active` when unset) and its `## NEXT` resume lead, and emits a TOON list. The plan sibling of
|
|
7
7
|
[`discover-specs`](../discover-specs/README.md).
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/discover-plans.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/discover-plans.mjs`](./scripts/discover-plans.mjs)
|
|
11
|
+
- **Tests:** [`src/discover-plans/discover-plans.test.mts`](../../src/discover-plans/discover-plans.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
node scripts/discover-plans.mjs --root . --format toon
|
|
@@ -12,7 +12,7 @@ The concrete engine for SDD **plan discovery**. It
|
|
|
12
12
|
locates the **resumable missions** in a repo — the mission **plan briefs** under `.agents/plans` —
|
|
13
13
|
and returns each one's CR ref, name, todo tally, and `## NEXT` resume lead, **without reading the
|
|
14
14
|
rest of the body**, so a consumer (the **gateway**, on entry) can offer resume cheaply. It carries a
|
|
15
|
-
self-contained `.
|
|
15
|
+
self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11). It is the **plan** sibling
|
|
16
16
|
of `discover-specs`: that engine finds **specs** by status shape; this one finds **missions** by
|
|
17
17
|
their briefs.
|
|
18
18
|
|
|
@@ -1,12 +1,137 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/discover-plans/discover-plans.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.2","entry":"skills/discover-plans/scripts/discover-plans.mjs","commands":{}};
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
//#region src/discover-plans/discover-plans.mts
|
|
8
|
+
const TODO_STATUSES = /* @__PURE__ */ new Set([
|
|
9
|
+
"pending",
|
|
10
|
+
"in_progress",
|
|
11
|
+
"completed"
|
|
12
|
+
]);
|
|
13
|
+
/** The default mission dispatch status — the meaning of an absent top-level `status`. */
|
|
14
|
+
const DEFAULT_PLAN_STATUS = "active";
|
|
15
|
+
function parsePlanFrontmatter(text) {
|
|
16
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
17
|
+
if (!m) return null;
|
|
18
|
+
const fm = {
|
|
19
|
+
name: "",
|
|
20
|
+
status: DEFAULT_PLAN_STATUS,
|
|
21
|
+
total: 0,
|
|
22
|
+
completed: 0,
|
|
23
|
+
inProgress: 0,
|
|
24
|
+
pending: 0
|
|
25
|
+
};
|
|
26
|
+
let inTodos = false;
|
|
27
|
+
for (const raw of m[1].split("\n")) {
|
|
28
|
+
const line = raw.replace(/\r$/, "");
|
|
29
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
30
|
+
const indent = line.length - line.trimStart().length;
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (indent === 0) {
|
|
33
|
+
inTodos = false;
|
|
34
|
+
const [key, ...rest] = trimmed.split(":");
|
|
35
|
+
const value = rest.join(":").trim();
|
|
36
|
+
if (key === "name") fm.name = unquote(value);
|
|
37
|
+
else if (key === "status") {
|
|
38
|
+
const v = unquote(value);
|
|
39
|
+
if (v !== "") fm.status = v;
|
|
40
|
+
} else if (key === "todos") inTodos = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (inTodos) {
|
|
44
|
+
const sm = /^(?:-\s+)?status:\s*(.+)$/.exec(trimmed);
|
|
45
|
+
if (sm) {
|
|
46
|
+
const status = unquote(sm[1].trim());
|
|
47
|
+
fm.total++;
|
|
48
|
+
if (status === "completed") fm.completed++;
|
|
49
|
+
else if (status === "in_progress") fm.inProgress++;
|
|
50
|
+
else if (status === "pending") fm.pending++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return fm;
|
|
55
|
+
}
|
|
56
|
+
function unquote(v) {
|
|
57
|
+
return v.replace(/^["']|["']$/g, "");
|
|
58
|
+
}
|
|
59
|
+
function nextLead(text) {
|
|
60
|
+
const lines = text.split("\n");
|
|
61
|
+
let i = lines.findIndex((l) => /^##\s+NEXT\b/i.test(l.trim()));
|
|
62
|
+
if (i === -1) return "";
|
|
63
|
+
for (i += 1; i < lines.length; i++) {
|
|
64
|
+
const l = lines[i].replace(/\r$/, "").trim();
|
|
65
|
+
if (l === "") continue;
|
|
66
|
+
if (l.startsWith("#")) return "";
|
|
67
|
+
const cleaned = l.replace(/^[-*]\s+/, "").replace(/\*\*/g, "").trim();
|
|
68
|
+
return cleaned.length > 200 ? `${cleaned.slice(0, 197)}...` : cleaned;
|
|
69
|
+
}
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
function discoverPlanFiles(root) {
|
|
73
|
+
const dir = join(root, ".agents", "plans");
|
|
74
|
+
if (!existsSync(dir)) return [];
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".plan.md")).map((e) => e.name).sort();
|
|
82
|
+
}
|
|
83
|
+
function collectPlans(root) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const name of discoverPlanFiles(root)) {
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = readFileSync(join(root, ".agents", "plans", name), "utf8");
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const fm = parsePlanFrontmatter(text);
|
|
93
|
+
if (!fm) continue;
|
|
94
|
+
out.push({
|
|
95
|
+
cr: name.replace(/\.plan\.md$/, ""),
|
|
96
|
+
name: fm.name,
|
|
97
|
+
total: fm.total,
|
|
98
|
+
completed: fm.completed,
|
|
99
|
+
inProgress: fm.inProgress,
|
|
100
|
+
status: fm.status,
|
|
101
|
+
next: nextLead(text)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return out.sort((a, b) => a.cr < b.cr ? -1 : a.cr > b.cr ? 1 : 0);
|
|
105
|
+
}
|
|
106
|
+
function filterByStatus(plans, status) {
|
|
107
|
+
return plans.filter((p) => p.status === status);
|
|
108
|
+
}
|
|
109
|
+
const COLUMNS = [
|
|
110
|
+
"cr",
|
|
111
|
+
"name",
|
|
112
|
+
"total",
|
|
113
|
+
"completed",
|
|
114
|
+
"inProgress",
|
|
115
|
+
"status",
|
|
116
|
+
"next"
|
|
117
|
+
];
|
|
118
|
+
function toonField(v) {
|
|
119
|
+
if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
|
|
120
|
+
return v;
|
|
121
|
+
}
|
|
122
|
+
function toToon(plans) {
|
|
123
|
+
return [`plans[${plans.length}]{${COLUMNS.join(",")}}:`, ...plans.map((p) => ` ${COLUMNS.map((c) => toonField(String(p[c]))).join(",")}`)].join("\n");
|
|
124
|
+
}
|
|
125
|
+
function main(argv) {
|
|
126
|
+
const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
|
|
127
|
+
const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
|
|
128
|
+
const statusFilter = argv.includes("--status") ? argv[argv.indexOf("--status") + 1] : void 0;
|
|
129
|
+
let plans = collectPlans(root);
|
|
130
|
+
if (statusFilter !== void 0) plans = filterByStatus(plans, statusFilter);
|
|
131
|
+
const out = format === "json" ? JSON.stringify(plans, null, 2) : toToon(plans);
|
|
132
|
+
process.stdout.write(`${out}\n`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
136
|
+
//#endregion
|
|
137
|
+
export { DEFAULT_PLAN_STATUS, TODO_STATUSES, collectPlans, filterByStatus, main, nextLead, parsePlanFrontmatter, toToon };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# discover-specs
|
|
2
2
|
|
|
3
|
-
The concrete engine for SDD **spec discovery**. A non-user-invocable skill carrying a self-contained `.
|
|
3
|
+
The concrete engine for SDD **spec discovery**. A non-user-invocable skill carrying a self-contained `.mjs`
|
|
4
4
|
script that scans the three fixed SDD spec locations — plus any opt-in extra anchors declared in
|
|
5
5
|
`.agents/sdd/spec-anchors.toml` (ADR-0019, curated via `manage-spec-anchors`) — filters candidates by
|
|
6
6
|
the lifecycle `status` shape, parses each `spec.md`'s frontmatter only, and emits a TOON list of the
|
|
7
7
|
specs found.
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/discover-specs.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/discover-specs.mjs`](./scripts/discover-specs.mjs)
|
|
11
|
+
- **Tests:** [`src/discover-specs/discover-specs.test.mts`](../../src/discover-specs/discover-specs.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
node scripts/discover-specs.mjs --root . --format toon
|
|
@@ -11,7 +11,7 @@ metadata:
|
|
|
11
11
|
The concrete engine for SDD **spec discovery**. It locates the
|
|
12
12
|
project specs in a repo and returns each one's frontmatter — **without reading any spec body** — so a
|
|
13
13
|
consumer (the **gateway**, corpus tooling) can route on `status` / `project-path` / `approval`
|
|
14
|
-
cheaply. It carries a self-contained `.
|
|
14
|
+
cheaply. It carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11).
|
|
15
15
|
|
|
16
16
|
## Recognition — location-bounded and shape-confirmed
|
|
17
17
|
|