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,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 };
|