cyber-sdd 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/agents/sdd-scanner.md +1 -1
  4. package/package.json +11 -13
  5. package/plugin.json +55 -0
  6. package/skills/align-spec/SKILL.md +1 -1
  7. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  8. package/skills/blast-estimate/README.md +3 -3
  9. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  10. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  11. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  12. package/skills/check-plan-safety/SKILL.md +2 -3
  13. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  14. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  15. package/skills/check-project-specs/SKILL.md +1 -1
  16. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  17. package/skills/check-retired-terms/SKILL.md +2 -3
  18. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  19. package/skills/check-scenario-overlap/SKILL.md +1 -2
  20. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  21. package/skills/check-spec-references/SKILL.md +2 -2
  22. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  23. package/skills/check-spec-structure/SKILL.md +1 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  25. package/skills/collision-ladder/README.md +2 -2
  26. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  27. package/skills/combat-log-governance/SKILL.md +1 -1
  28. package/skills/concept-index/SKILL.md +1 -1
  29. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  30. package/skills/discover-plans/README.md +3 -3
  31. package/skills/discover-plans/SKILL.md +1 -1
  32. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  33. package/skills/discover-specs/README.md +3 -3
  34. package/skills/discover-specs/SKILL.md +1 -1
  35. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  36. package/skills/doctrine-loop/SKILL.md +1 -1
  37. package/skills/gate-validation-governance/SKILL.md +1 -1
  38. package/skills/init/README.md +2 -2
  39. package/skills/init/scripts/wire-statusline.mjs +168 -12
  40. package/skills/manage-ignore/README.md +3 -3
  41. package/skills/manage-ignore/SKILL.md +1 -2
  42. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  43. package/skills/manage-scenario-bridge/README.md +3 -3
  44. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  45. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  46. package/skills/manage-spec-anchors/README.md +3 -3
  47. package/skills/manage-spec-anchors/SKILL.md +1 -1
  48. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  49. package/skills/mission-graph/README.md +2 -2
  50. package/skills/mission-graph/SKILL.md +1 -1
  51. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  52. package/skills/pause-mission/SKILL.md +1 -1
  53. package/skills/place-node/SKILL.md +2 -2
  54. package/skills/place-node/scripts/place-node.mjs +117 -12
  55. package/skills/plan-retirement/README.md +2 -2
  56. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  57. package/skills/resolve-governances/README.md +3 -3
  58. package/skills/resolve-governances/SKILL.md +1 -1
  59. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  60. package/skills/resolve-tracking/SKILL.md +1 -1
  61. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  62. package/skills/scaffold-project-spec/SKILL.md +1 -1
  63. package/skills/spec-gate/README.md +12 -9
  64. package/skills/spec-gate/SKILL.md +2 -2
  65. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  66. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  67. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  68. package/skills/spec-producer-governance/SKILL.md +2 -2
  69. package/skills/start-mission/SKILL.md +1 -1
  70. package/skills/suite-format-governance/README.md +1 -1
  71. package/skills/suite-format-governance/SKILL.md +1 -1
  72. package/skills/touch-set-correction/README.md +5 -5
  73. package/skills/touch-set-correction/SKILL.md +1 -1
  74. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  75. package/skills/verify-scenarios/README.md +6 -6
  76. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  77. package/.plugin/pins.json +0 -3
  78. package/.plugin/plugin.json +0 -19
  79. package/dist/align-spec/scripts/align-spec.js +0 -102
  80. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  81. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  82. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  83. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  84. package/dist/concept-index/scripts/concept-index.js +0 -181
  85. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  86. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  87. package/dist/spec-gate/scripts/check-suite.js +0 -317
  88. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  89. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  90. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  91. package/skills/align-spec/scripts/align-spec.mts +0 -188
  92. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  93. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  94. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  95. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  96. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  97. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  98. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  99. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  100. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  102. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  103. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  104. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  105. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  106. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  108. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  109. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  110. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  111. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  112. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  113. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  114. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  115. package/skills/concept-index/scripts/concept-index.mts +0 -246
  116. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  117. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  118. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  119. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  120. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  121. package/skills/init/scripts/wire-statusline.mts +0 -279
  122. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  123. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  124. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  126. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  128. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  129. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  130. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  131. package/skills/place-node/scripts/place-node.mts +0 -158
  132. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  133. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  134. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  135. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  136. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  137. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  138. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  139. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  140. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  141. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  142. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  143. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  144. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  145. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  146. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  147. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  148. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  149. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,215 +0,0 @@
1
- #!/usr/bin/env node
2
- // discover-plans — intake/plan-discovery's concrete frontmatter engine. Scans the SDD
3
- // plan directory (.agents/plans) for *.plan.md mission briefs, parses each plan's
4
- // frontmatter (name + the todos tally by status) plus the lead line of its `## NEXT`
5
- // resume anchor, and emits a TOON list of the unretired / resumable missions.
6
- //
7
- // Recognition is location-bounded AND shape-confirmed:
8
- // - a file named `<cr-ref>.plan.md` sitting directly under <root>/.agents/plans/ is a
9
- // mission brief. A present plan brief is by definition UNRETIRED — the doctrine loop's
10
- // plan-retirement deletes a plan once its CR is done/merged AND distilled — so every
11
- // plan the scan finds is a resumable mission.
12
- // - shape: the file must carry a frontmatter block (the basic plan template intake
13
- // scaffolds). A `*.plan.md` with no frontmatter is skipped (a stray, not a brief).
14
- // Sibling files in the same directory that are NOT `*.plan.md` (a combat log `*.log.jsonl`,
15
- // a loose `*.md`) are never plan briefs and are ignored.
16
- //
17
- // Pure functions are exported for node:test; running the file directly drives the CLI.
18
- // No dependencies (the repo's node-≥23.6 / no-deps convention). --format json for a flat
19
- // array; default output is TOON (the token-efficient tabular form the gateway scans).
20
-
21
- import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
22
- import { join } from 'node:path'
23
- import { pathToFileURL } from 'node:url'
24
-
25
- export const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed'])
26
-
27
- /** The default mission dispatch status — the meaning of an absent top-level `status`. */
28
- export const DEFAULT_PLAN_STATUS = 'active'
29
-
30
- export interface PlanRecord {
31
- /** The CR ref — the plan's filename without the `.plan.md` suffix. */
32
- cr: string
33
- /** Frontmatter `name`, or '' when absent. */
34
- name: string
35
- /** Total todos in the brief. */
36
- total: number
37
- /** Todos with status `completed`. */
38
- completed: number
39
- /** Todos with status `in_progress`. */
40
- inProgress: number
41
- /** The mission dispatch flag — the top-level `status` (`active` when unset). */
42
- status: string
43
- /** The lead line of the `## NEXT` resume anchor, or '' when there is none. */
44
- next: string
45
- }
46
-
47
- export interface PlanFrontmatter {
48
- name: string
49
- /** The mission dispatch flag — the top-level `status` (`active` when unset). */
50
- status: string
51
- total: number
52
- completed: number
53
- inProgress: number
54
- pending: number
55
- }
56
-
57
- // ── Frontmatter parse (a minimal YAML subset — only the plan-brief schema) ──
58
- // Extracts the leading `---` … `---` block and reads `name` plus the `todos:` list's tally
59
- // by `status`. Returns null when there is no frontmatter block at all (a stray file).
60
- export function parsePlanFrontmatter(text: string): PlanFrontmatter | null {
61
- const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text)
62
- if (!m) return null
63
- const fm: PlanFrontmatter = {
64
- name: '',
65
- status: DEFAULT_PLAN_STATUS,
66
- total: 0,
67
- completed: 0,
68
- inProgress: 0,
69
- pending: 0,
70
- }
71
- let inTodos = false // currently inside the top-level `todos:` block
72
- for (const raw of m[1].split('\n')) {
73
- const line = raw.replace(/\r$/, '')
74
- if (line.trim() === '' || line.trim().startsWith('#')) continue
75
- const indent = line.length - line.trimStart().length
76
- const trimmed = line.trim()
77
- if (indent === 0) {
78
- inTodos = false
79
- const [key, ...rest] = trimmed.split(':')
80
- const value = rest.join(':').trim()
81
- if (key === 'name') fm.name = unquote(value)
82
- // The top-level `status` is the plan's dispatch flag (distinct from a todo's
83
- // `status`, which sits indented inside `todos:`). An empty value stays the default.
84
- else if (key === 'status') {
85
- const v = unquote(value)
86
- if (v !== '') fm.status = v
87
- } else if (key === 'todos') inTodos = true // value is empty; the list follows, indented
88
- continue
89
- }
90
- // Inside the todos list — count each item's status. A list item's keys sit at
91
- // indent ≥ 2 (`- id:` / `status:`); the status line is `status: <value>`.
92
- if (inTodos) {
93
- const sm = /^(?:-\s+)?status:\s*(.+)$/.exec(trimmed)
94
- if (sm) {
95
- const status = unquote(sm[1].trim())
96
- fm.total++
97
- if (status === 'completed') fm.completed++
98
- else if (status === 'in_progress') fm.inProgress++
99
- else if (status === 'pending') fm.pending++
100
- }
101
- }
102
- }
103
- return fm
104
- }
105
-
106
- function unquote(v: string): string {
107
- return v.replace(/^["']|["']$/g, '')
108
- }
109
-
110
- // ── The `## NEXT` resume anchor lead ──
111
- // Find the `## NEXT` section (the resume anchor pause-mission writes) and return its first
112
- // content line — the next concrete action — trimmed of markdown bullet/emphasis markers and
113
- // capped. Returns '' when there is no NEXT section or it is empty. Body read is bounded to
114
- // this one section; the rest of the body is never parsed.
115
- export function nextLead(text: string): string {
116
- const lines = text.split('\n')
117
- let i = lines.findIndex((l) => /^##\s+NEXT\b/i.test(l.trim()))
118
- if (i === -1) return ''
119
- for (i += 1; i < lines.length; i++) {
120
- const l = lines[i].replace(/\r$/, '').trim()
121
- if (l === '') continue
122
- if (l.startsWith('#')) return '' // hit the next heading with no content between
123
- const cleaned = l
124
- .replace(/^[-*]\s+/, '')
125
- .replace(/\*\*/g, '')
126
- .trim()
127
- return cleaned.length > 200 ? `${cleaned.slice(0, 197)}...` : cleaned
128
- }
129
- return ''
130
- }
131
-
132
- // ── Scan ──
133
- // List the plan-brief filenames (relative names) directly under <root>/.agents/plans. Only
134
- // files ending `.plan.md` are briefs; a missing plans dir yields the empty set.
135
- function discoverPlanFiles(root: string): string[] {
136
- const dir = join(root, '.agents', 'plans')
137
- if (!existsSync(dir)) return []
138
- let entries: import('node:fs').Dirent[]
139
- try {
140
- entries = readdirSync(dir, { withFileTypes: true })
141
- } catch {
142
- return []
143
- }
144
- return entries
145
- .filter((e) => e.isFile() && e.name.endsWith('.plan.md'))
146
- .map((e) => e.name)
147
- .sort()
148
- }
149
-
150
- // ── Collect ──
151
- // The list of resumable missions: every plan brief carrying a frontmatter block, keyed by
152
- // its CR ref, with its todo tally and NEXT lead. Sorted by cr ref for stable output.
153
- export function collectPlans(root: string): PlanRecord[] {
154
- const out: PlanRecord[] = []
155
- for (const name of discoverPlanFiles(root)) {
156
- let text: string
157
- try {
158
- text = readFileSync(join(root, '.agents', 'plans', name), 'utf8')
159
- } catch {
160
- continue
161
- }
162
- const fm = parsePlanFrontmatter(text)
163
- if (!fm) continue // no frontmatter — a stray, not a brief
164
- out.push({
165
- cr: name.replace(/\.plan\.md$/, ''),
166
- name: fm.name,
167
- total: fm.total,
168
- completed: fm.completed,
169
- inProgress: fm.inProgress,
170
- status: fm.status,
171
- next: nextLead(text),
172
- })
173
- }
174
- return out.sort((a, b) => (a.cr < b.cr ? -1 : a.cr > b.cr ? 1 : 0))
175
- }
176
-
177
- // ── Filter ──
178
- // Narrow a plan set to one dispatch status — the opt-in selector the gateway's dispatch loop
179
- // uses to build the approved queue. Records already carry a concrete `status` (unset → the
180
- // default `active`), so a filter to `active` includes the unset briefs and a value no brief
181
- // carries (an off-enum or simply-absent status) yields the empty set.
182
- export function filterByStatus(plans: PlanRecord[], status: string): PlanRecord[] {
183
- return plans.filter((p) => p.status === status)
184
- }
185
-
186
- // ── Output ──
187
- const COLUMNS = ['cr', 'name', 'total', 'completed', 'inProgress', 'status', 'next'] as const
188
-
189
- // Quote a TOON field only when it carries the delimiter, a quote, or edge whitespace.
190
- function toonField(v: string): string {
191
- if (v === '' || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
192
- return v
193
- }
194
-
195
- export function toToon(plans: PlanRecord[]): string {
196
- const header = `plans[${plans.length}]{${COLUMNS.join(',')}}:`
197
- const rows = plans.map((p) => ` ${COLUMNS.map((c) => toonField(String(p[c]))).join(',')}`)
198
- return [header, ...rows].join('\n')
199
- }
200
-
201
- export function main(argv: string[]): number {
202
- const root = argv.includes('--root') ? (argv[argv.indexOf('--root') + 1] ?? '.') : '.'
203
- const format = argv.includes('--format') ? argv[argv.indexOf('--format') + 1] : 'toon'
204
- const statusFilter = argv.includes('--status') ? argv[argv.indexOf('--status') + 1] : undefined
205
- let plans = collectPlans(root)
206
- // Opt-in: `--status <value>` narrows to the dispatch queue; absent, no status filter is applied.
207
- if (statusFilter !== undefined) plans = filterByStatus(plans, statusFilter)
208
- const out = format === 'json' ? JSON.stringify(plans, null, 2) : toToon(plans)
209
- process.stdout.write(`${out}\n`)
210
- return 0
211
- }
212
-
213
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
214
- process.exit(main(process.argv.slice(2)))
215
- }
@@ -1,319 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- //#region skills/discover-specs/scripts/discover-specs.mts
6
- const LIFECYCLE_STATUSES = /* @__PURE__ */ new Set([
7
- "draft",
8
- "approved",
9
- "implemented",
10
- "deprecated"
11
- ]);
12
- const ROOT_PROJECT_NAME = "repo";
13
- const SKIP_DIRS = /* @__PURE__ */ new Set([
14
- "node_modules",
15
- ".git",
16
- "dist",
17
- ".turbo",
18
- ".next",
19
- "coverage"
20
- ]);
21
- /**
22
- * A directory that is itself a checkout is a DIFFERENT repository's tree, not part of this corpus.
23
- *
24
- * A name blocklist cannot express this: it can only say "this is called X", never "this is a
25
- * boundary". `SKIP_DIRS` already holds `.git`, so the walk skipped the METADATA directory while
26
- * descending straight into the checkout that directory marks — one level off from where the guard
27
- * was needed. Agent-harness worktree isolation checks this repo out inside itself, and the scan then
28
- * found the whole corpus once per worktree: 38 spec files where 10 exist, and every corpus-wide
29
- * guard silently auditing a tree nobody has.
30
- *
31
- * `.git` is a DIRECTORY in a clone and a FILE in a worktree or submodule, so both forms count.
32
- */
33
- function isNestedCheckout(abs) {
34
- return existsSync(join(abs, ".git"));
35
- }
36
- const ANCHORS_CONFIG = ".agents/sdd/spec-anchors.toml";
37
- function parseFrontmatter(text) {
38
- const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
39
- if (!m) return null;
40
- const fm = { approval: {} };
41
- let gate = null;
42
- for (const raw of m[1].split("\n")) {
43
- const line = raw.replace(/\r$/, "");
44
- if (line.trim() === "" || line.trim().startsWith("#")) continue;
45
- const indent = line.length - line.trimStart().length;
46
- const [key, ...rest] = line.trim().split(":");
47
- const value = rest.join(":").trim();
48
- if (indent === 0) {
49
- gate = null;
50
- if (key === "status") fm.status = unquote(value);
51
- else if (key === "project-path") fm.projectPath = unquote(value);
52
- else if (key === "name") fm.name = unquote(value);
53
- else if (key === "approval") gate = null;
54
- } else if (indent === 2 && value === "" && insideApproval(m[1], line)) gate = key;
55
- else if (indent >= 4 && key === "verdict" && gate) fm.approval[gate] = unquote(value);
56
- }
57
- return fm;
58
- }
59
- function insideApproval(block, line) {
60
- const lines = block.split("\n");
61
- const idx = lines.indexOf(line);
62
- for (let i = idx - 1; i >= 0; i--) {
63
- const l = lines[i].replace(/\r$/, "");
64
- if (l.trim() === "" || l.trim().startsWith("#")) continue;
65
- if (l.length - l.trimStart().length === 0) return l.trim().replace(/:.*$/, "") === "approval";
66
- }
67
- return false;
68
- }
69
- function unquote(v) {
70
- return v.replace(/^["']|["']$/g, "");
71
- }
72
- function classifyLocation(relPath) {
73
- const p = relPath.replace(/\\/g, "/");
74
- if (p === ".agents/spec/spec.md") return {
75
- pattern: "root-single",
76
- locationDir: ""
77
- };
78
- const multi = /^\.agents\/specs\/([^/]+)\/spec\.md$/.exec(p);
79
- if (multi) return {
80
- pattern: "root-multi",
81
- locationDir: multi[1]
82
- };
83
- const nested = /^(.+)\/\.agents\/spec\/spec\.md$/.exec(p);
84
- if (nested) return {
85
- pattern: "nested",
86
- locationDir: nested[1]
87
- };
88
- return null;
89
- }
90
- function deriveName(loc, fm) {
91
- if (fm.name) return {
92
- name: fm.name,
93
- nameSource: "declared"
94
- };
95
- if (loc.pattern === "root-single") return {
96
- name: ROOT_PROJECT_NAME,
97
- nameSource: "derived"
98
- };
99
- if (loc.pattern === "root-multi") return {
100
- name: loc.locationDir,
101
- nameSource: "derived"
102
- };
103
- if (loc.pattern === "extra" && loc.capturedName) return {
104
- name: loc.capturedName,
105
- nameSource: "derived"
106
- };
107
- return {
108
- name: loc.locationDir.split("/").pop() ?? loc.locationDir,
109
- nameSource: "guessed"
110
- };
111
- }
112
- function discoverSpecFiles(root) {
113
- const found = [];
114
- const walk = (relDir) => {
115
- let entries;
116
- try {
117
- entries = readdirSync(join(root, relDir), { withFileTypes: true });
118
- } catch {
119
- return;
120
- }
121
- for (const e of entries) {
122
- if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
123
- const childRel = relDir ? `${relDir}/${e.name}` : e.name;
124
- if (isNestedCheckout(join(root, childRel))) continue;
125
- if (e.name === ".agents") {
126
- probeAgents(root, childRel, found);
127
- continue;
128
- }
129
- walk(childRel);
130
- }
131
- };
132
- walk("");
133
- return found;
134
- }
135
- function probeAgents(root, agentsRel, found) {
136
- const single = `${agentsRel}/spec/spec.md`;
137
- if (existsSync(join(root, single))) found.push(single);
138
- if (agentsRel === ".agents") {
139
- const specsDir = join(root, ".agents", "specs");
140
- let projects;
141
- try {
142
- projects = readdirSync(specsDir, { withFileTypes: true });
143
- } catch {
144
- projects = [];
145
- }
146
- for (const p of projects) {
147
- if (!p.isDirectory()) continue;
148
- const rel = `.agents/specs/${p.name}/spec.md`;
149
- if (existsSync(join(root, rel))) found.push(rel);
150
- }
151
- }
152
- }
153
- function parseAnchorsToml(text) {
154
- const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text);
155
- if (!m) return [];
156
- const out = [];
157
- for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim());
158
- return out.filter((s) => s !== "");
159
- }
160
- function readAnchors(root) {
161
- const file = join(root, ANCHORS_CONFIG);
162
- if (!existsSync(file)) return [];
163
- try {
164
- return parseAnchorsToml(readFileSync(file, "utf8"));
165
- } catch {
166
- process.stderr.write(`discover-specs: ignoring unreadable ${ANCHORS_CONFIG}\n`);
167
- return [];
168
- }
169
- }
170
- function collectDescendants(root, startDir) {
171
- const out = [startDir];
172
- let entries;
173
- try {
174
- entries = readdirSync(join(root, startDir), { withFileTypes: true });
175
- } catch {
176
- return out;
177
- }
178
- for (const e of entries) {
179
- if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
180
- const childRel = startDir ? `${startDir}/${e.name}` : e.name;
181
- if (isNestedCheckout(join(root, childRel))) continue;
182
- out.push(...collectDescendants(root, childRel));
183
- }
184
- return out;
185
- }
186
- function expandAnchor(root, pattern) {
187
- const segs = pattern.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
188
- let frontier = [{ dir: "" }];
189
- for (const seg of segs) {
190
- const next = [];
191
- if (seg === "**") {
192
- for (const node of frontier) for (const dir of collectDescendants(root, node.dir)) next.push({
193
- dir,
194
- capturedName: node.capturedName
195
- });
196
- frontier = next;
197
- continue;
198
- }
199
- const isGlob = seg === "*" || seg === "<project>";
200
- for (const node of frontier) if (isGlob) {
201
- let entries;
202
- try {
203
- entries = readdirSync(join(root, node.dir), { withFileTypes: true });
204
- } catch {
205
- continue;
206
- }
207
- for (const e of entries) {
208
- if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
209
- const childRel = node.dir ? `${node.dir}/${e.name}` : e.name;
210
- if (isNestedCheckout(join(root, childRel))) continue;
211
- next.push({
212
- dir: childRel,
213
- capturedName: seg === "<project>" ? e.name : node.capturedName
214
- });
215
- }
216
- } else next.push({
217
- dir: node.dir ? `${node.dir}/${seg}` : seg,
218
- capturedName: node.capturedName
219
- });
220
- frontier = next;
221
- }
222
- const out = [];
223
- for (const node of frontier) {
224
- const rel = node.dir ? `${node.dir}/spec.md` : "spec.md";
225
- if (existsSync(join(root, rel))) out.push({
226
- rel,
227
- capturedName: node.capturedName
228
- });
229
- }
230
- return out;
231
- }
232
- function recordFor(root, rel, loc) {
233
- let fm;
234
- try {
235
- fm = parseFrontmatter(readFileSync(join(root, rel), "utf8"));
236
- } catch {
237
- return null;
238
- }
239
- if (!fm?.status || !LIFECYCLE_STATUSES.has(fm.status)) return null;
240
- const { name, nameSource } = deriveName(loc, fm);
241
- return {
242
- path: rel.replace(/\/spec\.md$/, ""),
243
- name,
244
- nameSource,
245
- status: fm.status,
246
- projectPath: fm.projectPath ?? "",
247
- approvals: Object.entries(fm.approval).map(([gate, verdict]) => `${gate}:${verdict}`).join(";")
248
- };
249
- }
250
- function collectSpecs(root) {
251
- const out = [];
252
- const seen = /* @__PURE__ */ new Set();
253
- for (const rel of discoverSpecFiles(root)) {
254
- const loc = classifyLocation(rel);
255
- if (!loc) continue;
256
- const rec = recordFor(root, rel, loc);
257
- if (rec) {
258
- out.push(rec);
259
- seen.add(rel);
260
- }
261
- }
262
- for (const pattern of readAnchors(root)) for (const { rel, capturedName } of expandAnchor(root, pattern)) {
263
- if (seen.has(rel)) continue;
264
- const rec = recordFor(root, rel, {
265
- pattern: "extra",
266
- locationDir: rel.replace(/\/spec\.md$/, ""),
267
- capturedName
268
- });
269
- if (rec) {
270
- out.push(rec);
271
- seen.add(rel);
272
- }
273
- }
274
- return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
275
- }
276
- function resolveByName(specs, name) {
277
- const want = name.trim().toLowerCase();
278
- const hits = specs.filter((s) => s.name.toLowerCase() === want);
279
- if (hits.length === 0) return { kind: "none" };
280
- if (hits.length === 1) return {
281
- kind: "match",
282
- spec: hits[0]
283
- };
284
- return {
285
- kind: "ambiguous",
286
- candidates: hits
287
- };
288
- }
289
- const COLUMNS = [
290
- "path",
291
- "name",
292
- "nameSource",
293
- "status",
294
- "projectPath",
295
- "approvals"
296
- ];
297
- function toonField(v) {
298
- if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
299
- return v;
300
- }
301
- function toToon(specs) {
302
- return [`specs[${specs.length}]{${COLUMNS.join(",")}}:`, ...specs.map((s) => ` ${COLUMNS.map((c) => toonField(s[c])).join(",")}`)].join("\n");
303
- }
304
- function main(argv) {
305
- const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
306
- const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
307
- let specs = collectSpecs(root);
308
- if (argv.includes("--resolve")) {
309
- const name = argv[argv.indexOf("--resolve") + 1] ?? "";
310
- const r = resolveByName(specs, name);
311
- specs = r.kind === "match" ? [r.spec] : r.kind === "ambiguous" ? r.candidates : [];
312
- }
313
- const out = format === "json" ? JSON.stringify(specs, null, 2) : toToon(specs);
314
- process.stdout.write(`${out}\n`);
315
- return 0;
316
- }
317
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
318
- //#endregion
319
- export { LIFECYCLE_STATUSES, classifyLocation, collectSpecs, deriveName, discoverSpecFiles, expandAnchor, main, parseAnchorsToml, parseFrontmatter, readAnchors, resolveByName, toToon };