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
|
@@ -1,12 +1,321 @@
|
|
|
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-specs/discover-specs.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.2","entry":"skills/discover-specs/scripts/discover-specs.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-specs/discover-specs.mts
|
|
8
|
+
const LIFECYCLE_STATUSES = /* @__PURE__ */ new Set([
|
|
9
|
+
"draft",
|
|
10
|
+
"approved",
|
|
11
|
+
"implemented",
|
|
12
|
+
"deprecated"
|
|
13
|
+
]);
|
|
14
|
+
const ROOT_PROJECT_NAME = "repo";
|
|
15
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
16
|
+
"node_modules",
|
|
17
|
+
".git",
|
|
18
|
+
"dist",
|
|
19
|
+
".turbo",
|
|
20
|
+
".next",
|
|
21
|
+
"coverage"
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* A directory that is itself a checkout is a DIFFERENT repository's tree, not part of this corpus.
|
|
25
|
+
*
|
|
26
|
+
* A name blocklist cannot express this: it can only say "this is called X", never "this is a
|
|
27
|
+
* boundary". `SKIP_DIRS` already holds `.git`, so the walk skipped the METADATA directory while
|
|
28
|
+
* descending straight into the checkout that directory marks — one level off from where the guard
|
|
29
|
+
* was needed. Agent-harness worktree isolation checks this repo out inside itself, and the scan then
|
|
30
|
+
* found the whole corpus once per worktree: 38 spec files where 10 exist, and every corpus-wide
|
|
31
|
+
* guard silently auditing a tree nobody has.
|
|
32
|
+
*
|
|
33
|
+
* `.git` is a DIRECTORY in a clone and a FILE in a worktree or submodule, so both forms count.
|
|
34
|
+
*/
|
|
35
|
+
function isNestedCheckout(abs) {
|
|
36
|
+
return existsSync(join(abs, ".git"));
|
|
37
|
+
}
|
|
38
|
+
const ANCHORS_CONFIG = ".agents/sdd/spec-anchors.toml";
|
|
39
|
+
function parseFrontmatter(text) {
|
|
40
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
const fm = { approval: {} };
|
|
43
|
+
let gate = null;
|
|
44
|
+
for (const raw of m[1].split("\n")) {
|
|
45
|
+
const line = raw.replace(/\r$/, "");
|
|
46
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
47
|
+
const indent = line.length - line.trimStart().length;
|
|
48
|
+
const [key, ...rest] = line.trim().split(":");
|
|
49
|
+
const value = rest.join(":").trim();
|
|
50
|
+
if (indent === 0) {
|
|
51
|
+
gate = null;
|
|
52
|
+
if (key === "status") fm.status = unquote(value);
|
|
53
|
+
else if (key === "project-path") fm.projectPath = unquote(value);
|
|
54
|
+
else if (key === "name") fm.name = unquote(value);
|
|
55
|
+
else if (key === "approval") gate = null;
|
|
56
|
+
} else if (indent === 2 && value === "" && insideApproval(m[1], line)) gate = key;
|
|
57
|
+
else if (indent >= 4 && key === "verdict" && gate) fm.approval[gate] = unquote(value);
|
|
58
|
+
}
|
|
59
|
+
return fm;
|
|
60
|
+
}
|
|
61
|
+
function insideApproval(block, line) {
|
|
62
|
+
const lines = block.split("\n");
|
|
63
|
+
const idx = lines.indexOf(line);
|
|
64
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
65
|
+
const l = lines[i].replace(/\r$/, "");
|
|
66
|
+
if (l.trim() === "" || l.trim().startsWith("#")) continue;
|
|
67
|
+
if (l.length - l.trimStart().length === 0) return l.trim().replace(/:.*$/, "") === "approval";
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
function unquote(v) {
|
|
72
|
+
return v.replace(/^["']|["']$/g, "");
|
|
73
|
+
}
|
|
74
|
+
function classifyLocation(relPath) {
|
|
75
|
+
const p = relPath.replace(/\\/g, "/");
|
|
76
|
+
if (p === ".agents/spec/spec.md") return {
|
|
77
|
+
pattern: "root-single",
|
|
78
|
+
locationDir: ""
|
|
79
|
+
};
|
|
80
|
+
const multi = /^\.agents\/specs\/([^/]+)\/spec\.md$/.exec(p);
|
|
81
|
+
if (multi) return {
|
|
82
|
+
pattern: "root-multi",
|
|
83
|
+
locationDir: multi[1]
|
|
84
|
+
};
|
|
85
|
+
const nested = /^(.+)\/\.agents\/spec\/spec\.md$/.exec(p);
|
|
86
|
+
if (nested) return {
|
|
87
|
+
pattern: "nested",
|
|
88
|
+
locationDir: nested[1]
|
|
89
|
+
};
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function deriveName(loc, fm) {
|
|
93
|
+
if (fm.name) return {
|
|
94
|
+
name: fm.name,
|
|
95
|
+
nameSource: "declared"
|
|
96
|
+
};
|
|
97
|
+
if (loc.pattern === "root-single") return {
|
|
98
|
+
name: ROOT_PROJECT_NAME,
|
|
99
|
+
nameSource: "derived"
|
|
100
|
+
};
|
|
101
|
+
if (loc.pattern === "root-multi") return {
|
|
102
|
+
name: loc.locationDir,
|
|
103
|
+
nameSource: "derived"
|
|
104
|
+
};
|
|
105
|
+
if (loc.pattern === "extra" && loc.capturedName) return {
|
|
106
|
+
name: loc.capturedName,
|
|
107
|
+
nameSource: "derived"
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
name: loc.locationDir.split("/").pop() ?? loc.locationDir,
|
|
111
|
+
nameSource: "guessed"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function discoverSpecFiles(root) {
|
|
115
|
+
const found = [];
|
|
116
|
+
const walk = (relDir) => {
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = readdirSync(join(root, relDir), { withFileTypes: true });
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
for (const e of entries) {
|
|
124
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
125
|
+
const childRel = relDir ? `${relDir}/${e.name}` : e.name;
|
|
126
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
127
|
+
if (e.name === ".agents") {
|
|
128
|
+
probeAgents(root, childRel, found);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
walk(childRel);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
walk("");
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
function probeAgents(root, agentsRel, found) {
|
|
138
|
+
const single = `${agentsRel}/spec/spec.md`;
|
|
139
|
+
if (existsSync(join(root, single))) found.push(single);
|
|
140
|
+
if (agentsRel === ".agents") {
|
|
141
|
+
const specsDir = join(root, ".agents", "specs");
|
|
142
|
+
let projects;
|
|
143
|
+
try {
|
|
144
|
+
projects = readdirSync(specsDir, { withFileTypes: true });
|
|
145
|
+
} catch {
|
|
146
|
+
projects = [];
|
|
147
|
+
}
|
|
148
|
+
for (const p of projects) {
|
|
149
|
+
if (!p.isDirectory()) continue;
|
|
150
|
+
const rel = `.agents/specs/${p.name}/spec.md`;
|
|
151
|
+
if (existsSync(join(root, rel))) found.push(rel);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function parseAnchorsToml(text) {
|
|
156
|
+
const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text);
|
|
157
|
+
if (!m) return [];
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim());
|
|
160
|
+
return out.filter((s) => s !== "");
|
|
161
|
+
}
|
|
162
|
+
function readAnchors(root) {
|
|
163
|
+
const file = join(root, ANCHORS_CONFIG);
|
|
164
|
+
if (!existsSync(file)) return [];
|
|
165
|
+
try {
|
|
166
|
+
return parseAnchorsToml(readFileSync(file, "utf8"));
|
|
167
|
+
} catch {
|
|
168
|
+
process.stderr.write(`discover-specs: ignoring unreadable ${ANCHORS_CONFIG}\n`);
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function collectDescendants(root, startDir) {
|
|
173
|
+
const out = [startDir];
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = readdirSync(join(root, startDir), { withFileTypes: true });
|
|
177
|
+
} catch {
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
for (const e of entries) {
|
|
181
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
182
|
+
const childRel = startDir ? `${startDir}/${e.name}` : e.name;
|
|
183
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
184
|
+
out.push(...collectDescendants(root, childRel));
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function expandAnchor(root, pattern) {
|
|
189
|
+
const segs = pattern.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
190
|
+
let frontier = [{ dir: "" }];
|
|
191
|
+
for (const seg of segs) {
|
|
192
|
+
const next = [];
|
|
193
|
+
if (seg === "**") {
|
|
194
|
+
for (const node of frontier) for (const dir of collectDescendants(root, node.dir)) next.push({
|
|
195
|
+
dir,
|
|
196
|
+
capturedName: node.capturedName
|
|
197
|
+
});
|
|
198
|
+
frontier = next;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const isGlob = seg === "*" || seg === "<project>";
|
|
202
|
+
for (const node of frontier) if (isGlob) {
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
entries = readdirSync(join(root, node.dir), { withFileTypes: true });
|
|
206
|
+
} catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
for (const e of entries) {
|
|
210
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
211
|
+
const childRel = node.dir ? `${node.dir}/${e.name}` : e.name;
|
|
212
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
213
|
+
next.push({
|
|
214
|
+
dir: childRel,
|
|
215
|
+
capturedName: seg === "<project>" ? e.name : node.capturedName
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
} else next.push({
|
|
219
|
+
dir: node.dir ? `${node.dir}/${seg}` : seg,
|
|
220
|
+
capturedName: node.capturedName
|
|
221
|
+
});
|
|
222
|
+
frontier = next;
|
|
223
|
+
}
|
|
224
|
+
const out = [];
|
|
225
|
+
for (const node of frontier) {
|
|
226
|
+
const rel = node.dir ? `${node.dir}/spec.md` : "spec.md";
|
|
227
|
+
if (existsSync(join(root, rel))) out.push({
|
|
228
|
+
rel,
|
|
229
|
+
capturedName: node.capturedName
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
function recordFor(root, rel, loc) {
|
|
235
|
+
let fm;
|
|
236
|
+
try {
|
|
237
|
+
fm = parseFrontmatter(readFileSync(join(root, rel), "utf8"));
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (!fm?.status || !LIFECYCLE_STATUSES.has(fm.status)) return null;
|
|
242
|
+
const { name, nameSource } = deriveName(loc, fm);
|
|
243
|
+
return {
|
|
244
|
+
path: rel.replace(/\/spec\.md$/, ""),
|
|
245
|
+
name,
|
|
246
|
+
nameSource,
|
|
247
|
+
status: fm.status,
|
|
248
|
+
projectPath: fm.projectPath ?? "",
|
|
249
|
+
approvals: Object.entries(fm.approval).map(([gate, verdict]) => `${gate}:${verdict}`).join(";")
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function collectSpecs(root) {
|
|
253
|
+
const out = [];
|
|
254
|
+
const seen = /* @__PURE__ */ new Set();
|
|
255
|
+
for (const rel of discoverSpecFiles(root)) {
|
|
256
|
+
const loc = classifyLocation(rel);
|
|
257
|
+
if (!loc) continue;
|
|
258
|
+
const rec = recordFor(root, rel, loc);
|
|
259
|
+
if (rec) {
|
|
260
|
+
out.push(rec);
|
|
261
|
+
seen.add(rel);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const pattern of readAnchors(root)) for (const { rel, capturedName } of expandAnchor(root, pattern)) {
|
|
265
|
+
if (seen.has(rel)) continue;
|
|
266
|
+
const rec = recordFor(root, rel, {
|
|
267
|
+
pattern: "extra",
|
|
268
|
+
locationDir: rel.replace(/\/spec\.md$/, ""),
|
|
269
|
+
capturedName
|
|
270
|
+
});
|
|
271
|
+
if (rec) {
|
|
272
|
+
out.push(rec);
|
|
273
|
+
seen.add(rel);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
277
|
+
}
|
|
278
|
+
function resolveByName(specs, name) {
|
|
279
|
+
const want = name.trim().toLowerCase();
|
|
280
|
+
const hits = specs.filter((s) => s.name.toLowerCase() === want);
|
|
281
|
+
if (hits.length === 0) return { kind: "none" };
|
|
282
|
+
if (hits.length === 1) return {
|
|
283
|
+
kind: "match",
|
|
284
|
+
spec: hits[0]
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
kind: "ambiguous",
|
|
288
|
+
candidates: hits
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const COLUMNS = [
|
|
292
|
+
"path",
|
|
293
|
+
"name",
|
|
294
|
+
"nameSource",
|
|
295
|
+
"status",
|
|
296
|
+
"projectPath",
|
|
297
|
+
"approvals"
|
|
298
|
+
];
|
|
299
|
+
function toonField(v) {
|
|
300
|
+
if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
|
|
301
|
+
return v;
|
|
302
|
+
}
|
|
303
|
+
function toToon(specs) {
|
|
304
|
+
return [`specs[${specs.length}]{${COLUMNS.join(",")}}:`, ...specs.map((s) => ` ${COLUMNS.map((c) => toonField(s[c])).join(",")}`)].join("\n");
|
|
305
|
+
}
|
|
306
|
+
function main(argv) {
|
|
307
|
+
const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
|
|
308
|
+
const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
|
|
309
|
+
let specs = collectSpecs(root);
|
|
310
|
+
if (argv.includes("--resolve")) {
|
|
311
|
+
const name = argv[argv.indexOf("--resolve") + 1] ?? "";
|
|
312
|
+
const r = resolveByName(specs, name);
|
|
313
|
+
specs = r.kind === "match" ? [r.spec] : r.kind === "ambiguous" ? r.candidates : [];
|
|
314
|
+
}
|
|
315
|
+
const out = format === "json" ? JSON.stringify(specs, null, 2) : toToon(specs);
|
|
316
|
+
process.stdout.write(`${out}\n`);
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
320
|
+
//#endregion
|
|
321
|
+
export { LIFECYCLE_STATUSES, classifyLocation, collectSpecs, deriveName, discoverSpecFiles, expandAnchor, main, parseAnchorsToml, parseFrontmatter, readAnchors, resolveByName, toToon };
|
|
@@ -58,7 +58,7 @@ recorded as distilled).
|
|
|
58
58
|
**Determine distilled-ness by a structured JSONL parse, never a substring or regex text match.**
|
|
59
59
|
The ledger shards are JSONL (one JSON object per line). **Reuse the existing parse contract** rather
|
|
60
60
|
than hand-rolling one: run the exported **`distilledCrRefs`** engine from
|
|
61
|
-
`../plan-retirement/scripts/retire-plans.
|
|
61
|
+
`../plan-retirement/scripts/retire-plans.mjs` (its `--distilled` flag) over the project `ledger/` directory and membership-test
|
|
62
62
|
the mission's `<cr-ref>` against the set it returns, e.g.
|
|
63
63
|
|
|
64
64
|
```
|
|
@@ -13,7 +13,7 @@ conductor's autonomy bar (baked into `start-mission`), a hard floor + a three-di
|
|
|
13
13
|
|
|
14
14
|
## Legal-state tuples
|
|
15
15
|
|
|
16
|
-
The mechanical authority is `spec-gate/scripts/check-spec-state.
|
|
16
|
+
The mechanical authority is `spec-gate/scripts/check-spec-state.mjs` — run it
|
|
17
17
|
(`node <skill>/scripts/check-spec-state.mjs`) to enforce; if `node` is unavailable, apply the same
|
|
18
18
|
rules by reading frontmatter. The `(status, markers, .feature, approval)` tuple of the root
|
|
19
19
|
`spec.md` — plus the durable gate lines globbed from its sibling `ledger/` shards (a legacy `ledger.jsonl` still counts) — is **illegal** when:
|
package/skills/init/README.md
CHANGED
|
@@ -7,8 +7,8 @@ mission runs. A user-invocable setup skill (sibling to `start-mission` / `manage
|
|
|
7
7
|
and invokes no gate.
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/wire-statusline.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/wire-statusline.mjs`](./scripts/wire-statusline.mjs)
|
|
11
|
+
- **Tests:** [`src/init/wire-statusline.test.mts`](../../src/init/wire-statusline.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
node scripts/wire-statusline.mjs --root . --detect
|
|
@@ -1,12 +1,168 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/init/wire-statusline.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.2","entry":"skills/init/scripts/wire-statusline.mjs","commands":{}};
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
//#region src/init/wire-statusline.mts
|
|
9
|
+
const STATUS_FILE = ".agents/sdd/statusline";
|
|
10
|
+
const SETTINGS_FILE = ".claude/settings.json";
|
|
11
|
+
const GITIGNORE_FILE = ".gitignore";
|
|
12
|
+
const GLOBAL_SETTINGS_FILE = join(homedir(), ".claude", "settings.json");
|
|
13
|
+
const MARKER_BEGIN = "# sdd-statusline:begin";
|
|
14
|
+
const MARKER_END = "# sdd-statusline:end";
|
|
15
|
+
const MODE_PREFIX = "# sdd-statusline:mode:";
|
|
16
|
+
const ORIG_OPEN = "__sdd_orig() {";
|
|
17
|
+
const ORIG_CLOSE = "}";
|
|
18
|
+
const NO_BASE = ":";
|
|
19
|
+
function buildCommand(base, mode) {
|
|
20
|
+
const origBody = base && base.trim() !== "" ? base : NO_BASE;
|
|
21
|
+
const render = mode === "own-line" ? `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s\\n%s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi` : `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s | %s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi`;
|
|
22
|
+
return [
|
|
23
|
+
MARKER_BEGIN,
|
|
24
|
+
`${MODE_PREFIX}${mode}`,
|
|
25
|
+
ORIG_OPEN,
|
|
26
|
+
origBody,
|
|
27
|
+
ORIG_CLOSE,
|
|
28
|
+
"__sdd_base=\"$(__sdd_orig 2>/dev/null)\"",
|
|
29
|
+
`__sdd_file="${STATUS_FILE}"`,
|
|
30
|
+
render,
|
|
31
|
+
MARKER_END
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
function parseCommand(command) {
|
|
35
|
+
if (!command?.includes(MARKER_BEGIN)) return { wired: false };
|
|
36
|
+
const modeMatch = new RegExp(`${MODE_PREFIX}(own-line|same-line)`).exec(command);
|
|
37
|
+
const origMatch = /__sdd_orig\(\) \{\n([\s\S]*?)\n\}/.exec(command);
|
|
38
|
+
const mode = modeMatch?.[1] ?? "own-line";
|
|
39
|
+
const origBody = origMatch?.[1] ?? NO_BASE;
|
|
40
|
+
return {
|
|
41
|
+
wired: true,
|
|
42
|
+
base: origBody === NO_BASE ? void 0 : origBody,
|
|
43
|
+
mode
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function baseToWrap(existingCommand) {
|
|
47
|
+
const parsed = parseCommand(existingCommand);
|
|
48
|
+
return parsed.wired ? parsed.base : existingCommand;
|
|
49
|
+
}
|
|
50
|
+
function readSettingsFile(file) {
|
|
51
|
+
if (!existsSync(file)) return {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readSettings(root) {
|
|
59
|
+
return readSettingsFile(join(root, SETTINGS_FILE));
|
|
60
|
+
}
|
|
61
|
+
function readStatusLineCommand(file) {
|
|
62
|
+
const command = readSettingsFile(file).statusLine?.command;
|
|
63
|
+
if (typeof command !== "string" || command.trim() === "") return void 0;
|
|
64
|
+
return command;
|
|
65
|
+
}
|
|
66
|
+
function writeSettings(root, settings) {
|
|
67
|
+
const file = join(root, SETTINGS_FILE);
|
|
68
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
69
|
+
writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
function composeStatusLine(root, mode, globalCommand) {
|
|
72
|
+
const settings = readSettings(root);
|
|
73
|
+
const existing = settings.statusLine?.command;
|
|
74
|
+
const hasProjectStatusLine = typeof existing === "string" && existing.trim() !== "";
|
|
75
|
+
const globalBaseComposed = !hasProjectStatusLine && globalCommand !== void 0;
|
|
76
|
+
const command = buildCommand(hasProjectStatusLine ? baseToWrap(existing) : globalCommand, mode);
|
|
77
|
+
if (existing === command) return {
|
|
78
|
+
changed: false,
|
|
79
|
+
command,
|
|
80
|
+
globalBaseComposed: false
|
|
81
|
+
};
|
|
82
|
+
settings.statusLine = {
|
|
83
|
+
...settings.statusLine,
|
|
84
|
+
type: "command",
|
|
85
|
+
command
|
|
86
|
+
};
|
|
87
|
+
writeSettings(root, settings);
|
|
88
|
+
return {
|
|
89
|
+
changed: true,
|
|
90
|
+
command,
|
|
91
|
+
globalBaseComposed
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function isGitRepo(root) {
|
|
95
|
+
return existsSync(join(root, ".git"));
|
|
96
|
+
}
|
|
97
|
+
function splitLines(text) {
|
|
98
|
+
const lines = text.split("\n");
|
|
99
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
function addGitignoreEntry(root) {
|
|
103
|
+
const file = join(root, GITIGNORE_FILE);
|
|
104
|
+
const lines = existsSync(file) ? splitLines(readFileSync(file, "utf8")) : [];
|
|
105
|
+
if (lines.some((l) => l.trim() === ".agents/sdd/statusline")) return { changed: false };
|
|
106
|
+
const next = [...lines, STATUS_FILE];
|
|
107
|
+
writeFileSync(file, `${next.join("\n")}\n`);
|
|
108
|
+
return { changed: true };
|
|
109
|
+
}
|
|
110
|
+
function detectStatusLines(root, globalSettingsFile) {
|
|
111
|
+
const projectCommand = readStatusLineCommand(join(root, SETTINGS_FILE));
|
|
112
|
+
const project = projectCommand === void 0 ? "absent" : parseCommand(projectCommand).wired ? "wired" : "foreign";
|
|
113
|
+
const global = readStatusLineCommand(globalSettingsFile ?? GLOBAL_SETTINGS_FILE) !== void 0 ? "present" : "absent";
|
|
114
|
+
return {
|
|
115
|
+
project,
|
|
116
|
+
global,
|
|
117
|
+
shadow: project === "absent" && global === "present"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function wireStatusline(root, mode, opts) {
|
|
121
|
+
return {
|
|
122
|
+
settings: composeStatusLine(root, mode, opts?.globalBase ?? true ? readStatusLineCommand(opts?.globalSettingsFile ?? GLOBAL_SETTINGS_FILE) : void 0),
|
|
123
|
+
gitignore: isGitRepo(root) ? {
|
|
124
|
+
...addGitignoreEntry(root),
|
|
125
|
+
skipped: false
|
|
126
|
+
} : {
|
|
127
|
+
changed: false,
|
|
128
|
+
skipped: true
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function flag(argv, name) {
|
|
133
|
+
const i = argv.indexOf(name);
|
|
134
|
+
return i === -1 ? void 0 : argv[i + 1];
|
|
135
|
+
}
|
|
136
|
+
function main(argv) {
|
|
137
|
+
const root = flag(argv, "--root") ?? ".";
|
|
138
|
+
const globalSettingsFile = flag(argv, "--global-settings");
|
|
139
|
+
const w = (s) => process.stdout.write(`${s}\n`);
|
|
140
|
+
if (argv.includes("--detect")) {
|
|
141
|
+
const result = detectStatusLines(root, globalSettingsFile);
|
|
142
|
+
w(`project statusLine: ${result.project}`);
|
|
143
|
+
w(`global statusLine: ${result.global}`);
|
|
144
|
+
w(result.shadow ? "shadow risk: yes — wiring would shadow the global statusline; compose it as the base (default) or pass --no-global-base to shadow deliberately" : "shadow risk: no");
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
if (argv.includes("--wire")) {
|
|
148
|
+
const mode = flag(argv, "--mode");
|
|
149
|
+
if (mode !== "own-line" && mode !== "same-line") {
|
|
150
|
+
w("refused: --mode must be own-line or same-line");
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
const result = wireStatusline(root, mode, {
|
|
154
|
+
globalSettingsFile,
|
|
155
|
+
globalBase: !argv.includes("--no-global-base")
|
|
156
|
+
});
|
|
157
|
+
w(result.settings.changed ? `wired statusLine (${mode})` : "statusLine already wired (unchanged)");
|
|
158
|
+
if (result.settings.globalBaseComposed) w("composed the global statusLine command as the wrapped base");
|
|
159
|
+
if (result.gitignore.skipped) w("not a git repo — gitignore skipped");
|
|
160
|
+
else w(result.gitignore.changed ? `added ${STATUS_FILE} to .gitignore` : ".gitignore already up to date");
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
w("usage: wire-statusline --root <dir> --wire --mode own-line|same-line [--global-settings <file>] [--no-global-base]\n wire-statusline --root <dir> --detect [--global-settings <file>]");
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
167
|
+
//#endregion
|
|
168
|
+
export { GITIGNORE_FILE, GLOBAL_SETTINGS_FILE, SETTINGS_FILE, STATUS_FILE, addGitignoreEntry, buildCommand, composeStatusLine, detectStatusLines, isGitRepo, main, parseCommand, readStatusLineCommand, wireStatusline };
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
The concrete engine for `.agents/sdd/.sddignore` — the optional gitignore-syntax file
|
|
4
4
|
`resolve-tracking` reads to decide whether an artifact is tracked or ignored. A non-user-invocable
|
|
5
|
-
skill, loaded in-session by the `manage` gateway (Housekeeping), carrying a self-contained `.
|
|
5
|
+
skill, loaded in-session by the `manage` gateway (Housekeeping), carrying a self-contained `.mjs`
|
|
6
6
|
script that lists / adds / removes / induces / previews the ignore rules so users never hand-edit the
|
|
7
7
|
file. Order is meaningful (last-match-wins), so rules are never re-sorted.
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/manage-ignore.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/manage-ignore.mjs`](./scripts/manage-ignore.mjs)
|
|
11
|
+
- **Tests:** [`src/manage-ignore/manage-ignore.test.mts`](../../src/manage-ignore/manage-ignore.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
node scripts/manage-ignore.mjs --list
|
|
@@ -12,8 +12,7 @@ The concrete engine for `.agents/sdd/.sddignore` — the optional gitignore-synt
|
|
|
12
12
|
`resolve-tracking` consults to decide whether an artifact is **tracked** or **ignored**. It is the
|
|
13
13
|
**write** side of the ignore file (`resolve-tracking` is the read side). It exists so a user curates
|
|
14
14
|
the ignore rules through a clean interface instead of hand-editing the file. Loaded **in-session** by
|
|
15
|
-
the `manage` gateway (Housekeeping group); it carries a self-contained `.
|
|
16
|
-
node-≥23.6 / no-deps convention).
|
|
15
|
+
the `manage` gateway (Housekeeping group); it carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11).
|
|
17
16
|
|
|
18
17
|
## The file it curates
|
|
19
18
|
|