cyber-sdd 0.6.1 → 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 (136) 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/agents/sdd-spec-judge.md +3 -3
  5. package/agents/sdd-warden.md +8 -0
  6. package/package.json +11 -13
  7. package/plugin.json +55 -0
  8. package/skills/align-spec/README.md +3 -3
  9. package/skills/align-spec/SKILL.md +2 -2
  10. package/skills/align-spec/scripts/align-spec.mjs +6755 -0
  11. package/skills/architect-impl-governance/SKILL.md +3 -1
  12. package/skills/architect-spec-governance/SKILL.md +5 -1
  13. package/skills/blast-estimate/README.md +4 -4
  14. package/skills/blast-estimate/SKILL.md +1 -1
  15. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -0
  16. package/skills/check-field-mandates/SKILL.md +1 -1
  17. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -0
  18. package/skills/check-partition-quality/SKILL.md +1 -1
  19. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -0
  20. package/skills/check-plan-safety/README.md +3 -3
  21. package/skills/check-plan-safety/SKILL.md +3 -4
  22. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -0
  23. package/skills/check-plugin-manifests/SKILL.md +1 -1
  24. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -0
  25. package/skills/check-project-specs/README.md +3 -3
  26. package/skills/check-project-specs/SKILL.md +3 -3
  27. package/{dist/check-project-specs/scripts/check-project-specs.js → skills/check-project-specs/scripts/check-project-specs.mjs} +300 -18
  28. package/skills/check-retired-terms/README.md +2 -2
  29. package/skills/check-retired-terms/SKILL.md +4 -5
  30. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -0
  31. package/skills/check-scenario-overlap/README.md +2 -2
  32. package/skills/check-scenario-overlap/SKILL.md +2 -3
  33. package/{dist/check-scenario-overlap/scripts/check-scenario-overlap.js → skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs} +3 -1
  34. package/skills/check-spec-references/README.md +1 -1
  35. package/skills/check-spec-references/SKILL.md +3 -3
  36. package/{dist/check-spec-references/scripts/check-spec-references.js → skills/check-spec-references/scripts/check-spec-references.mjs} +3 -1
  37. package/skills/check-spec-structure/README.md +2 -2
  38. package/skills/check-spec-structure/SKILL.md +2 -3
  39. package/{dist/check-spec-structure/scripts/check-spec-structure.js → skills/check-spec-structure/scripts/check-spec-structure.mjs} +3 -1
  40. package/skills/collision-ladder/README.md +2 -2
  41. package/skills/collision-ladder/SKILL.md +2 -2
  42. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -0
  43. package/skills/combat-log-governance/SKILL.md +1 -1
  44. package/skills/concept-index/README.md +2 -2
  45. package/skills/concept-index/SKILL.md +2 -2
  46. package/{dist/concept-index/scripts/concept-index.js → skills/concept-index/scripts/concept-index.mjs} +3 -1
  47. package/skills/discover-plans/README.md +5 -5
  48. package/skills/discover-plans/SKILL.md +2 -2
  49. package/skills/discover-plans/scripts/discover-plans.mjs +137 -0
  50. package/skills/discover-specs/README.md +4 -4
  51. package/skills/discover-specs/SKILL.md +2 -2
  52. package/{dist/discover-specs/scripts/discover-specs.js → skills/discover-specs/scripts/discover-specs.mjs} +4 -1
  53. package/skills/doctrine-loop/SKILL.md +2 -2
  54. package/skills/formation-loop/SKILL.md +10 -0
  55. package/skills/gate-validation-governance/SKILL.md +2 -2
  56. package/skills/init/README.md +5 -5
  57. package/skills/init/SKILL.md +3 -3
  58. package/skills/init/scripts/wire-statusline.mjs +168 -0
  59. package/skills/manage-ignore/README.md +8 -8
  60. package/skills/manage-ignore/SKILL.md +2 -3
  61. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -0
  62. package/skills/manage-scenario-bridge/README.md +6 -6
  63. package/skills/manage-scenario-bridge/SKILL.md +2 -2
  64. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -0
  65. package/skills/manage-spec-anchors/README.md +7 -7
  66. package/skills/manage-spec-anchors/SKILL.md +2 -2
  67. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -0
  68. package/skills/mission-graph/README.md +2 -2
  69. package/skills/mission-graph/SKILL.md +10 -10
  70. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -0
  71. package/skills/pause-mission/SKILL.md +2 -2
  72. package/skills/place-node/README.md +1 -1
  73. package/skills/place-node/SKILL.md +3 -3
  74. package/skills/place-node/scripts/place-node.mjs +117 -0
  75. package/skills/plan-retirement/README.md +2 -2
  76. package/skills/plan-retirement/SKILL.md +1 -1
  77. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -0
  78. package/skills/resolve-governances/README.md +4 -4
  79. package/skills/resolve-governances/SKILL.md +2 -2
  80. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -0
  81. package/skills/resolve-tracking/SKILL.md +2 -2
  82. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -0
  83. package/skills/scaffold-project-spec/SKILL.md +3 -3
  84. package/skills/sdd/SKILL.md +1 -1
  85. package/skills/spec-gate/README.md +12 -9
  86. package/skills/spec-gate/SKILL.md +6 -6
  87. package/{dist/spec-gate/scripts/check-spec-state.js → skills/spec-gate/scripts/check-spec-state.mjs} +3 -1
  88. package/{dist/spec-gate/vendor/gherkin-cli.js → skills/spec-gate/scripts/check-suite.mjs} +319 -188
  89. package/skills/spec-gate/{vendor/gherkin-cli.mjs → scripts/classify-edit-class.mjs} +243 -209
  90. package/skills/spec-producer-governance/SKILL.md +4 -4
  91. package/skills/spec-structure-governance/README.md +4 -2
  92. package/skills/spec-structure-governance/SKILL.md +42 -7
  93. package/skills/start-mission/SKILL.md +2 -2
  94. package/skills/suite-format-governance/README.md +1 -1
  95. package/skills/suite-format-governance/SKILL.md +1 -1
  96. package/skills/touch-set-correction/README.md +5 -5
  97. package/skills/touch-set-correction/SKILL.md +2 -2
  98. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -0
  99. package/skills/verify-scenarios/README.md +9 -9
  100. package/skills/verify-scenarios/SKILL.md +2 -2
  101. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -0
  102. package/.plugin/pins.json +0 -3
  103. package/.plugin/plugin.json +0 -19
  104. package/dist/align-spec/scripts/align-spec.js +0 -102
  105. package/dist/spec-gate/scripts/check-suite.js +0 -317
  106. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  107. package/skills/align-spec/scripts/align-spec.mts +0 -188
  108. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  109. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  110. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  111. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  112. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  113. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -369
  114. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  115. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  116. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  117. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  118. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -660
  119. package/skills/concept-index/scripts/concept-index.mts +0 -246
  120. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  121. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  124. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  125. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  126. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  127. package/skills/place-node/scripts/place-node.mts +0 -158
  128. package/skills/plan-retirement/scripts/retire-plans.mts +0 -199
  129. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  130. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  131. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  132. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  133. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  134. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  135. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -420
  136. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ // Compiled by `universal-plugin plugin build` from src/check-plugin-manifests/check-plugin-manifests.mts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs","commands":{}};
4
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
5
+ import { dirname, join, relative, sep } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region src/check-plugin-manifests/check-plugin-manifests.mts
8
+ /**
9
+ * Directories that hold a manifest one level below the plugin root.
10
+ *
11
+ * This is a path-shape list, not a provenance one: `.plugin/` holds the **canonical** manifest and
12
+ * the other two hold its generated vendor copies, but all three sit one level down, so a pointer
13
+ * inside any of them resolves against the parent. Naming it for vendors would contradict the
14
+ * spec's own vocabulary and has already misled one reader.
15
+ */
16
+ const MANIFEST_DIRS = [
17
+ ".plugin",
18
+ ".claude-plugin",
19
+ ".codex-plugin"
20
+ ];
21
+ const MANIFEST_NAME = "plugin.json";
22
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
23
+ /**
24
+ * Every manifest at a conventional location under `root`, repo-relative and path-sorted.
25
+ *
26
+ * A manifest is a `plugin.json` either inside a vendor directory, or sitting beside a
27
+ * `package.json` (a package-root manifest). Sorted so a sweep's report order is deterministic.
28
+ * A symlink is included: it resolves to a real manifest, and a broken one must surface as
29
+ * unreadable rather than vanish from the sweep.
30
+ */
31
+ /** Directory entries, or none where the directory cannot be read. */
32
+ function safeReaddir(dir) {
33
+ try {
34
+ return readdirSync(dir, { withFileTypes: true });
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+ function discoverManifests(root) {
40
+ const found = [];
41
+ const walk = (dir) => {
42
+ for (const e of safeReaddir(dir)) {
43
+ const full = join(dir, e.name);
44
+ if (e.isDirectory()) {
45
+ if (!SKIP_DIRS.has(e.name)) walk(full);
46
+ continue;
47
+ }
48
+ if (e.name !== MANIFEST_NAME) continue;
49
+ const parent = dirname(full);
50
+ if (MANIFEST_DIRS.includes(parent.split(sep).pop() ?? "") || existsSync(join(parent, "package.json"))) found.push(full);
51
+ }
52
+ };
53
+ walk(root);
54
+ return found.map((f) => relative(root, f).split(sep).join("/")).sort();
55
+ }
56
+ /**
57
+ * The plugin root a manifest's pointers resolve against — NOT the manifest's own directory.
58
+ *
59
+ * A vendor manifest sits one level down, and its `./skills` still names the plugin's skills
60
+ * directory. A guard resolving relative to the file it just read reports every vendor manifest as
61
+ * entirely broken.
62
+ */
63
+ function pluginRootOf(manifestRelPath) {
64
+ const parts = manifestRelPath.split("/");
65
+ const parentName = parts[parts.length - 2];
66
+ const drop = parentName !== void 0 && MANIFEST_DIRS.includes(parentName) ? 2 : 1;
67
+ return parts.slice(0, parts.length - drop).join("/");
68
+ }
69
+ /** Every `./`-prefixed string value in the manifest, paired with the key it sits under. */
70
+ function extractPointers(manifest) {
71
+ const out = [];
72
+ const visit = (value, key) => {
73
+ if (typeof value === "string") {
74
+ if (value.startsWith("./")) out.push({
75
+ key,
76
+ pointer: value
77
+ });
78
+ return;
79
+ }
80
+ if (Array.isArray(value)) {
81
+ for (const item of value) visit(item, key);
82
+ return;
83
+ }
84
+ if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) visit(v, k);
85
+ };
86
+ visit(manifest, "");
87
+ return out;
88
+ }
89
+ /**
90
+ * The nearest `package.json` at or above the plugin root, and whether it publishes.
91
+ *
92
+ * A package publishes when it is not `private` AND declares a `files` allowlist. A `private`
93
+ * package ships no tarball, so its `files` constrains nothing. A package declaring no `files` at
94
+ * all ships everything, so there is no allowlist to be outside of — npm's own semantic, and
95
+ * inverting it manufactures a finding against every pointer in the package.
96
+ */
97
+ function owningPackage(root, pluginRoot) {
98
+ let dir = pluginRoot;
99
+ for (;;) {
100
+ const candidate = dir === "" ? "package.json" : `${dir}/package.json`;
101
+ const abs = join(root, candidate);
102
+ if (existsSync(abs)) try {
103
+ const pkg = JSON.parse(readFileSync(abs, "utf8"));
104
+ const files = Array.isArray(pkg.files) ? pkg.files.filter((f) => typeof f === "string") : null;
105
+ const packs = pkg.private !== true;
106
+ return {
107
+ path: candidate,
108
+ packs,
109
+ publishes: packs && files !== null,
110
+ files: files ?? []
111
+ };
112
+ } catch {
113
+ return {
114
+ path: candidate,
115
+ packs: false,
116
+ publishes: false,
117
+ files: []
118
+ };
119
+ }
120
+ if (dir === "") return null;
121
+ dir = dir.split("/").slice(0, -1).join("/");
122
+ }
123
+ }
124
+ /**
125
+ * Whether a `files` allowlist covers a pointer, compared on the leading path segment.
126
+ *
127
+ * Both sides are normalized past a leading `./`, because an allowlist may legitimately spell an
128
+ * entry either way and a pointer always carries the prefix — comparing them raw reports a shipped
129
+ * component as unpublished.
130
+ *
131
+ * A `!`-prefixed entry is an exclusion and needs no special case: its leading `!` is part of its
132
+ * first segment, so it can never equal a real segment and is never read as an inclusion.
133
+ */
134
+ function filesCovers(files, pointer) {
135
+ const head = (path) => path.replace(/^\.\//, "").split("/")[0];
136
+ const segment = head(pointer);
137
+ if (segment === void 0 || segment === "") return true;
138
+ return files.some((entry) => {
139
+ const h = head(entry);
140
+ return h === "*" || h === "**" || h === segment;
141
+ });
142
+ }
143
+ /** Whether a package that packs a tarball ships the package-relative path `./<rel>`. */
144
+ function ships(pkg, rel) {
145
+ return pkg?.packs === true && (!pkg.publishes || filesCovers(pkg.files, `./${rel}`));
146
+ }
147
+ /** Every symbolic link at or below `abs` — the path itself included, links never followed. */
148
+ function findSymlinks(abs) {
149
+ let stat;
150
+ try {
151
+ stat = lstatSync(abs);
152
+ } catch {
153
+ return [];
154
+ }
155
+ if (stat.isSymbolicLink()) return [abs];
156
+ if (!stat.isDirectory()) return [];
157
+ return safeReaddir(abs).filter((e) => !SKIP_DIRS.has(e.name)).flatMap((e) => findSymlinks(join(abs, e.name)));
158
+ }
159
+ function sweep(root, manifests) {
160
+ const findings = [];
161
+ const linksSeen = /* @__PURE__ */ new Set();
162
+ const reportLinks = (rel, abs, key, pointer) => {
163
+ for (const link of findSymlinks(abs)) {
164
+ const linkRel = relative(root, link).split(sep).join("/");
165
+ if (linksSeen.has(linkRel)) continue;
166
+ linksSeen.add(linkRel);
167
+ findings.push({
168
+ kind: "symlink",
169
+ manifest: rel,
170
+ ...key === void 0 ? {} : {
171
+ key,
172
+ pointer
173
+ },
174
+ detail: `${linkRel} is a symbolic link — the registry rejects a package that ships one`
175
+ });
176
+ }
177
+ };
178
+ for (const rel of manifests) {
179
+ let parsed;
180
+ try {
181
+ parsed = JSON.parse(readFileSync(join(root, rel), "utf8"));
182
+ } catch {
183
+ findings.push({
184
+ kind: "unreadable",
185
+ manifest: rel,
186
+ detail: "not readable as JSON"
187
+ });
188
+ continue;
189
+ }
190
+ const pluginRoot = pluginRootOf(rel);
191
+ const pkg = owningPackage(root, pluginRoot);
192
+ const pkgDir = pkg ? dirname(pkg.path) : "";
193
+ const fromPkg = (path) => relative(join(root, pkgDir), join(root, path)).split(sep).join("/");
194
+ if (ships(pkg, fromPkg(rel))) reportLinks(rel, join(root, rel));
195
+ for (const { key, pointer } of extractPointers(parsed)) {
196
+ const target = join(root, pluginRoot, pointer);
197
+ if (!existsSync(target)) {
198
+ findings.push({
199
+ kind: "unresolved",
200
+ manifest: rel,
201
+ key,
202
+ pointer,
203
+ detail: "names no path on disk"
204
+ });
205
+ continue;
206
+ }
207
+ if (pkg?.publishes && !filesCovers(pkg.files, pointer)) {
208
+ findings.push({
209
+ kind: "unpublished",
210
+ manifest: rel,
211
+ key,
212
+ pointer,
213
+ detail: `declared but not published — ${pkg.path} files omits it`
214
+ });
215
+ continue;
216
+ }
217
+ if (ships(pkg, fromPkg(join(pluginRoot, pointer)))) reportLinks(rel, target, key, pointer);
218
+ }
219
+ }
220
+ return findings;
221
+ }
222
+ function formatReport(manifests, findings) {
223
+ if (manifests.length === 0) return "check-plugin-manifests: no plugin manifest found\n";
224
+ if (findings.length === 0) return `check-plugin-manifests: ${manifests.length} manifest(s) OK\n`;
225
+ return `${findings.map((f) => ` ${f.kind.padEnd(11)} ${f.manifest}${f.key ? ` [${f.key}] ${f.pointer}` : ""} — ${f.detail}`).join("\n")}\ncheck-plugin-manifests: ${findings.length} finding(s) across ${manifests.length} manifest(s)\n`;
226
+ }
227
+ function main(argv) {
228
+ let root = ".";
229
+ for (let i = 0; i < argv.length; i++) {
230
+ const a = argv[i];
231
+ if (a === "--root") {
232
+ const next = argv[++i];
233
+ if (next === void 0) {
234
+ process.stderr.write("check-plugin-manifests: --root needs a directory\n");
235
+ return 1;
236
+ }
237
+ root = next;
238
+ continue;
239
+ }
240
+ process.stderr.write(`check-plugin-manifests: unrecognized flag ${a}\n`);
241
+ return 1;
242
+ }
243
+ const manifests = discoverManifests(root);
244
+ const findings = sweep(root, manifests);
245
+ process.stdout.write(formatReport(manifests, findings));
246
+ return findings.length === 0 ? 0 : 1;
247
+ }
248
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
249
+ //#endregion
250
+ export { discoverManifests, extractPointers, filesCovers, findSymlinks, formatReport, main, owningPackage, pluginRootOf, sweep };
@@ -3,9 +3,9 @@
3
3
  Runs every project-spec check — over the whole corpus, or over one project's spec.
4
4
 
5
5
  ```bash
6
- node scripts/check-project-specs.mts --corpus # every project-spec + the coverage guard
7
- node scripts/check-project-specs.mts # one project, resolved from cwd
8
- node scripts/check-project-specs.mts --project <dir> # one project, explicit dir
6
+ node scripts/check-project-specs.mjs --corpus # every project-spec + the coverage guard
7
+ node scripts/check-project-specs.mjs # one project, resolved from cwd
8
+ node scripts/check-project-specs.mjs --project <dir> # one project, explicit dir
9
9
  ```
10
10
 
11
11
  The **corpus** scope is the repo's commit floor, wired as the root `check:specs`; it is total by
@@ -10,7 +10,7 @@ metadata:
10
10
 
11
11
  The **entrypoint** for the project-spec checks — the harness behind `corpus/spec-floor`. It resolves
12
12
  which spec governs which project and runs each project-spec engine against it. It carries a
13
- self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention).
13
+ self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11).
14
14
 
15
15
  Two scopes:
16
16
 
@@ -48,8 +48,8 @@ mapping and no path is ever written into a package's scripts.
48
48
  ## Run it
49
49
 
50
50
  ```bash
51
- node "<skill>/scripts/check-project-specs.mts" --corpus # the whole corpus — the commit floor
52
- node "<skill>/scripts/check-project-specs.mts" [--project <dir>] # one project-spec
51
+ node "<skill>/scripts/check-project-specs.mjs" --corpus # the whole corpus — the commit floor
52
+ node "<skill>/scripts/check-project-specs.mjs" [--project <dir>] # one project-spec
53
53
  ```
54
54
 
55
55
  The corpus scope is wired as the repo's root `check:specs`; the project scope is wired as each
@@ -1,25 +1,309 @@
1
1
  #!/usr/bin/env node
2
- import { collectSpecs, discoverSpecFiles, parseFrontmatter } from "../../discover-specs/scripts/discover-specs.js";
2
+ // Compiled by `universal-plugin plugin build` from src/check-project-specs/check-project-specs.mts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/check-project-specs/scripts/check-project-specs.mjs","commands":{}};
3
4
  import { execFileSync } from "node:child_process";
4
- import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
5
6
  import { dirname, join, relative, resolve } from "node:path";
6
7
  import { fileURLToPath, pathToFileURL } from "node:url";
7
- //#region skills/check-project-specs/scripts/check-project-specs.mts
8
+ //#region src/discover-specs/discover-specs.mts
9
+ const LIFECYCLE_STATUSES = /* @__PURE__ */ new Set([
10
+ "draft",
11
+ "approved",
12
+ "implemented",
13
+ "deprecated"
14
+ ]);
15
+ const ROOT_PROJECT_NAME = "repo";
16
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
17
+ "node_modules",
18
+ ".git",
19
+ "dist",
20
+ ".turbo",
21
+ ".next",
22
+ "coverage"
23
+ ]);
24
+ /**
25
+ * A directory that is itself a checkout is a DIFFERENT repository's tree, not part of this corpus.
26
+ *
27
+ * A name blocklist cannot express this: it can only say "this is called X", never "this is a
28
+ * boundary". `SKIP_DIRS` already holds `.git`, so the walk skipped the METADATA directory while
29
+ * descending straight into the checkout that directory marks — one level off from where the guard
30
+ * was needed. Agent-harness worktree isolation checks this repo out inside itself, and the scan then
31
+ * found the whole corpus once per worktree: 38 spec files where 10 exist, and every corpus-wide
32
+ * guard silently auditing a tree nobody has.
33
+ *
34
+ * `.git` is a DIRECTORY in a clone and a FILE in a worktree or submodule, so both forms count.
35
+ */
36
+ function isNestedCheckout(abs) {
37
+ return existsSync(join(abs, ".git"));
38
+ }
39
+ const ANCHORS_CONFIG = ".agents/sdd/spec-anchors.toml";
40
+ function parseFrontmatter(text) {
41
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
42
+ if (!m) return null;
43
+ const fm = { approval: {} };
44
+ let gate = null;
45
+ for (const raw of m[1].split("\n")) {
46
+ const line = raw.replace(/\r$/, "");
47
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
48
+ const indent = line.length - line.trimStart().length;
49
+ const [key, ...rest] = line.trim().split(":");
50
+ const value = rest.join(":").trim();
51
+ if (indent === 0) {
52
+ gate = null;
53
+ if (key === "status") fm.status = unquote(value);
54
+ else if (key === "project-path") fm.projectPath = unquote(value);
55
+ else if (key === "name") fm.name = unquote(value);
56
+ else if (key === "approval") gate = null;
57
+ } else if (indent === 2 && value === "" && insideApproval(m[1], line)) gate = key;
58
+ else if (indent >= 4 && key === "verdict" && gate) fm.approval[gate] = unquote(value);
59
+ }
60
+ return fm;
61
+ }
62
+ function insideApproval(block, line) {
63
+ const lines = block.split("\n");
64
+ const idx = lines.indexOf(line);
65
+ for (let i = idx - 1; i >= 0; i--) {
66
+ const l = lines[i].replace(/\r$/, "");
67
+ if (l.trim() === "" || l.trim().startsWith("#")) continue;
68
+ if (l.length - l.trimStart().length === 0) return l.trim().replace(/:.*$/, "") === "approval";
69
+ }
70
+ return false;
71
+ }
72
+ function unquote(v) {
73
+ return v.replace(/^["']|["']$/g, "");
74
+ }
75
+ function classifyLocation(relPath) {
76
+ const p = relPath.replace(/\\/g, "/");
77
+ if (p === ".agents/spec/spec.md") return {
78
+ pattern: "root-single",
79
+ locationDir: ""
80
+ };
81
+ const multi = /^\.agents\/specs\/([^/]+)\/spec\.md$/.exec(p);
82
+ if (multi) return {
83
+ pattern: "root-multi",
84
+ locationDir: multi[1]
85
+ };
86
+ const nested = /^(.+)\/\.agents\/spec\/spec\.md$/.exec(p);
87
+ if (nested) return {
88
+ pattern: "nested",
89
+ locationDir: nested[1]
90
+ };
91
+ return null;
92
+ }
93
+ function deriveName(loc, fm) {
94
+ if (fm.name) return {
95
+ name: fm.name,
96
+ nameSource: "declared"
97
+ };
98
+ if (loc.pattern === "root-single") return {
99
+ name: ROOT_PROJECT_NAME,
100
+ nameSource: "derived"
101
+ };
102
+ if (loc.pattern === "root-multi") return {
103
+ name: loc.locationDir,
104
+ nameSource: "derived"
105
+ };
106
+ if (loc.pattern === "extra" && loc.capturedName) return {
107
+ name: loc.capturedName,
108
+ nameSource: "derived"
109
+ };
110
+ return {
111
+ name: loc.locationDir.split("/").pop() ?? loc.locationDir,
112
+ nameSource: "guessed"
113
+ };
114
+ }
115
+ function discoverSpecFiles(root) {
116
+ const found = [];
117
+ const walk = (relDir) => {
118
+ let entries;
119
+ try {
120
+ entries = readdirSync(join(root, relDir), { withFileTypes: true });
121
+ } catch {
122
+ return;
123
+ }
124
+ for (const e of entries) {
125
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
126
+ const childRel = relDir ? `${relDir}/${e.name}` : e.name;
127
+ if (isNestedCheckout(join(root, childRel))) continue;
128
+ if (e.name === ".agents") {
129
+ probeAgents(root, childRel, found);
130
+ continue;
131
+ }
132
+ walk(childRel);
133
+ }
134
+ };
135
+ walk("");
136
+ return found;
137
+ }
138
+ function probeAgents(root, agentsRel, found) {
139
+ const single = `${agentsRel}/spec/spec.md`;
140
+ if (existsSync(join(root, single))) found.push(single);
141
+ if (agentsRel === ".agents") {
142
+ const specsDir = join(root, ".agents", "specs");
143
+ let projects;
144
+ try {
145
+ projects = readdirSync(specsDir, { withFileTypes: true });
146
+ } catch {
147
+ projects = [];
148
+ }
149
+ for (const p of projects) {
150
+ if (!p.isDirectory()) continue;
151
+ const rel = `.agents/specs/${p.name}/spec.md`;
152
+ if (existsSync(join(root, rel))) found.push(rel);
153
+ }
154
+ }
155
+ }
156
+ function parseAnchorsToml(text) {
157
+ const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text);
158
+ if (!m) return [];
159
+ const out = [];
160
+ for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim());
161
+ return out.filter((s) => s !== "");
162
+ }
163
+ function readAnchors(root) {
164
+ const file = join(root, ANCHORS_CONFIG);
165
+ if (!existsSync(file)) return [];
166
+ try {
167
+ return parseAnchorsToml(readFileSync(file, "utf8"));
168
+ } catch {
169
+ process.stderr.write(`discover-specs: ignoring unreadable ${ANCHORS_CONFIG}\n`);
170
+ return [];
171
+ }
172
+ }
173
+ function collectDescendants(root, startDir) {
174
+ const out = [startDir];
175
+ let entries;
176
+ try {
177
+ entries = readdirSync(join(root, startDir), { withFileTypes: true });
178
+ } catch {
179
+ return out;
180
+ }
181
+ for (const e of entries) {
182
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
183
+ const childRel = startDir ? `${startDir}/${e.name}` : e.name;
184
+ if (isNestedCheckout(join(root, childRel))) continue;
185
+ out.push(...collectDescendants(root, childRel));
186
+ }
187
+ return out;
188
+ }
189
+ function expandAnchor(root, pattern) {
190
+ const segs = pattern.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
191
+ let frontier = [{ dir: "" }];
192
+ for (const seg of segs) {
193
+ const next = [];
194
+ if (seg === "**") {
195
+ for (const node of frontier) for (const dir of collectDescendants(root, node.dir)) next.push({
196
+ dir,
197
+ capturedName: node.capturedName
198
+ });
199
+ frontier = next;
200
+ continue;
201
+ }
202
+ const isGlob = seg === "*" || seg === "<project>";
203
+ for (const node of frontier) if (isGlob) {
204
+ let entries;
205
+ try {
206
+ entries = readdirSync(join(root, node.dir), { withFileTypes: true });
207
+ } catch {
208
+ continue;
209
+ }
210
+ for (const e of entries) {
211
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
212
+ const childRel = node.dir ? `${node.dir}/${e.name}` : e.name;
213
+ if (isNestedCheckout(join(root, childRel))) continue;
214
+ next.push({
215
+ dir: childRel,
216
+ capturedName: seg === "<project>" ? e.name : node.capturedName
217
+ });
218
+ }
219
+ } else next.push({
220
+ dir: node.dir ? `${node.dir}/${seg}` : seg,
221
+ capturedName: node.capturedName
222
+ });
223
+ frontier = next;
224
+ }
225
+ const out = [];
226
+ for (const node of frontier) {
227
+ const rel = node.dir ? `${node.dir}/spec.md` : "spec.md";
228
+ if (existsSync(join(root, rel))) out.push({
229
+ rel,
230
+ capturedName: node.capturedName
231
+ });
232
+ }
233
+ return out;
234
+ }
235
+ function recordFor(root, rel, loc) {
236
+ let fm;
237
+ try {
238
+ fm = parseFrontmatter(readFileSync(join(root, rel), "utf8"));
239
+ } catch {
240
+ return null;
241
+ }
242
+ if (!fm?.status || !LIFECYCLE_STATUSES.has(fm.status)) return null;
243
+ const { name, nameSource } = deriveName(loc, fm);
244
+ return {
245
+ path: rel.replace(/\/spec\.md$/, ""),
246
+ name,
247
+ nameSource,
248
+ status: fm.status,
249
+ projectPath: fm.projectPath ?? "",
250
+ approvals: Object.entries(fm.approval).map(([gate, verdict]) => `${gate}:${verdict}`).join(";")
251
+ };
252
+ }
253
+ function collectSpecs(root) {
254
+ const out = [];
255
+ const seen = /* @__PURE__ */ new Set();
256
+ for (const rel of discoverSpecFiles(root)) {
257
+ const loc = classifyLocation(rel);
258
+ if (!loc) continue;
259
+ const rec = recordFor(root, rel, loc);
260
+ if (rec) {
261
+ out.push(rec);
262
+ seen.add(rel);
263
+ }
264
+ }
265
+ for (const pattern of readAnchors(root)) for (const { rel, capturedName } of expandAnchor(root, pattern)) {
266
+ if (seen.has(rel)) continue;
267
+ const rec = recordFor(root, rel, {
268
+ pattern: "extra",
269
+ locationDir: rel.replace(/\/spec\.md$/, ""),
270
+ capturedName
271
+ });
272
+ if (rec) {
273
+ out.push(rec);
274
+ seen.add(rel);
275
+ }
276
+ }
277
+ return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
278
+ }
279
+ if (process.argv[1] && false);
280
+ //#endregion
281
+ //#region src/sibling-engine.mts
282
+ /** The path of `<skill>/<engine>` for an engine running from the file `here`. */
283
+ function siblingEngine(here, skill, engine) {
284
+ if (here.endsWith(".mts")) return join(dirname(here), "..", skill, `${engine}.mts`);
285
+ return join(dirname(here), "..", "..", skill, "scripts", `${engine}.mjs`);
286
+ }
287
+ //#endregion
288
+ //#region src/check-project-specs/check-project-specs.mts
8
289
  const HERE = fileURLToPath(import.meta.url);
9
290
  const ENGINES = [
10
291
  {
11
292
  name: "check-spec-state",
12
- script: "spec-gate/scripts/check-spec-state.mts",
293
+ skill: "spec-gate",
294
+ engine: "check-spec-state",
13
295
  args: (d) => ["--root", d]
14
296
  },
15
297
  {
16
298
  name: "check-suite",
17
- script: "spec-gate/scripts/check-suite.mts",
299
+ skill: "spec-gate",
300
+ engine: "check-suite",
18
301
  args: (d) => ["--root", d]
19
302
  },
20
303
  {
21
304
  name: "concept-index",
22
- script: "concept-index/scripts/concept-index.mts",
305
+ skill: "concept-index",
306
+ engine: "concept-index",
23
307
  args: (d) => [
24
308
  "--spec-dir",
25
309
  d,
@@ -28,7 +312,8 @@ const ENGINES = [
28
312
  },
29
313
  {
30
314
  name: "check-spec-structure",
31
- script: "check-spec-structure/scripts/check-spec-structure.mts",
315
+ skill: "check-spec-structure",
316
+ engine: "check-spec-structure",
32
317
  args: (d) => [
33
318
  "--spec-dir",
34
319
  d,
@@ -37,7 +322,8 @@ const ENGINES = [
37
322
  },
38
323
  {
39
324
  name: "align-spec",
40
- script: "align-spec/scripts/align-spec.mts",
325
+ skill: "align-spec",
326
+ engine: "align-spec",
41
327
  args: (d) => [
42
328
  "--spec-dir",
43
329
  d,
@@ -46,12 +332,14 @@ const ENGINES = [
46
332
  },
47
333
  {
48
334
  name: "check-spec-references",
49
- script: "check-spec-references/scripts/check-spec-references.mts",
335
+ skill: "check-spec-references",
336
+ engine: "check-spec-references",
50
337
  args: (d) => ["--spec-dir", d]
51
338
  },
52
339
  {
53
340
  name: "check-scenario-overlap",
54
- script: "check-scenario-overlap/scripts/check-scenario-overlap.mts",
341
+ skill: "check-scenario-overlap",
342
+ engine: "check-scenario-overlap",
55
343
  args: (d) => [
56
344
  "--spec-dir",
57
345
  d,
@@ -59,15 +347,9 @@ const ENGINES = [
59
347
  ]
60
348
  }
61
349
  ];
62
- /**
63
- * Where an engine runs from. The .mts source runs its siblings in the skills tree; the
64
- * published bin runs from dist/, which mirrors that tree compiled to .js (tsdown.config.ts)
65
- * — Node will not strip types under node_modules, so the bin must never reach back into
66
- * the .mts tree.
67
- */
350
+ /** Where an engine runs from: beside this one, in the same form (see sibling-engine.mts). */
68
351
  function enginePath(e, here = HERE) {
69
- const skillsDir = resolve(dirname(here), "../..");
70
- return join(skillsDir, here.endsWith(".mts") ? e.script : e.script.replace(/\.mts$/, ".js"));
352
+ return siblingEngine(here, e.skill, e.engine);
71
353
  }
72
354
  /** Walk up from `start` for the workspace marker. Returns '' when not found. */
73
355
  function findRepoRoot(start) {
@@ -5,8 +5,8 @@ corpus-wide sweep**. Scans every git-tracked file for a literal, case-sensitive
5
5
  term registered in `.agents/sdd/retired-terms.toml` as retired by a design decision.
6
6
 
7
7
  ```bash
8
- node scripts/check-retired-terms.mts --root . # the verify-time sweep
9
- node scripts/check-retired-terms.mts --root . --list # what is registered
8
+ node scripts/check-retired-terms.mjs --root . # the verify-time sweep
9
+ node scripts/check-retired-terms.mjs --root . --list # what is registered
10
10
  ```
11
11
 
12
12
  Reports every survivor as `file:line:term — replace with: <replacement>`, then a count, and exits