cyber-sdd 0.0.0
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 +17 -0
- package/.codex-plugin/plugin.json +17 -0
- package/.plugin/plugin.json +17 -0
- package/README.md +159 -0
- package/agents/sdd-automaton.md +97 -0
- package/agents/sdd-impl-judge.md +214 -0
- package/agents/sdd-scanner.md +120 -0
- package/agents/sdd-spec-judge.md +224 -0
- package/agents/sdd-warden.md +101 -0
- package/package.json +24 -0
- package/skills/align-spec/README.md +20 -0
- package/skills/align-spec/SKILL.md +111 -0
- package/skills/align-spec/scripts/align-spec.mts +187 -0
- package/skills/architect-impl-governance/README.md +46 -0
- package/skills/architect-impl-governance/SKILL.md +45 -0
- package/skills/architect-spec-governance/README.md +48 -0
- package/skills/architect-spec-governance/SKILL.md +59 -0
- package/skills/blast-estimate/README.md +47 -0
- package/skills/blast-estimate/SKILL.md +133 -0
- package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
- package/skills/builder-impl-governance/README.md +47 -0
- package/skills/builder-impl-governance/SKILL.md +47 -0
- package/skills/builder-spec-governance/README.md +49 -0
- package/skills/builder-spec-governance/SKILL.md +36 -0
- package/skills/check-partition-quality/README.md +22 -0
- package/skills/check-partition-quality/SKILL.md +51 -0
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
- package/skills/check-plan-safety/README.md +17 -0
- package/skills/check-plan-safety/SKILL.md +60 -0
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
- package/skills/check-project-specs/README.md +19 -0
- package/skills/check-project-specs/SKILL.md +69 -0
- package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
- package/skills/check-scenario-overlap/README.md +19 -0
- package/skills/check-scenario-overlap/SKILL.md +74 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
- package/skills/check-spec-structure/README.md +17 -0
- package/skills/check-spec-structure/SKILL.md +66 -0
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
- package/skills/collision-ladder/README.md +18 -0
- package/skills/collision-ladder/SKILL.md +83 -0
- package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
- package/skills/combat-log-governance/README.md +13 -0
- package/skills/combat-log-governance/SKILL.md +257 -0
- package/skills/concept-index/README.md +13 -0
- package/skills/concept-index/SKILL.md +38 -0
- package/skills/concept-index/scripts/concept-index.mts +245 -0
- package/skills/discover-plans/README.md +16 -0
- package/skills/discover-plans/SKILL.md +74 -0
- package/skills/discover-plans/scripts/discover-plans.mts +212 -0
- package/skills/discover-specs/README.md +15 -0
- package/skills/discover-specs/SKILL.md +76 -0
- package/skills/discover-specs/scripts/discover-specs.mts +396 -0
- package/skills/doctrine-loop/README.md +15 -0
- package/skills/doctrine-loop/SKILL.md +97 -0
- package/skills/formation-loop/README.md +17 -0
- package/skills/formation-loop/SKILL.md +140 -0
- package/skills/gate-validation-governance/README.md +12 -0
- package/skills/gate-validation-governance/SKILL.md +87 -0
- package/skills/impl-producer-governance/README.md +48 -0
- package/skills/impl-producer-governance/SKILL.md +85 -0
- package/skills/init/README.md +27 -0
- package/skills/init/SKILL.md +68 -0
- package/skills/init/scripts/wire-statusline.mts +276 -0
- package/skills/lifecycle-governance/README.md +11 -0
- package/skills/lifecycle-governance/SKILL.md +168 -0
- package/skills/manage/README.md +9 -0
- package/skills/manage/SKILL.md +62 -0
- package/skills/manage-ignore/README.md +19 -0
- package/skills/manage-ignore/SKILL.md +52 -0
- package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
- package/skills/manage-scenario-bridge/README.md +20 -0
- package/skills/manage-scenario-bridge/SKILL.md +60 -0
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
- package/skills/manage-spec-anchors/README.md +18 -0
- package/skills/manage-spec-anchors/SKILL.md +56 -0
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
- package/skills/mission-graph/README.md +15 -0
- package/skills/mission-graph/SKILL.md +67 -0
- package/skills/mission-graph/scripts/mission-graph.mts +844 -0
- package/skills/oracle-spec-governance/README.md +45 -0
- package/skills/oracle-spec-governance/SKILL.md +45 -0
- package/skills/ownership-governance/README.md +65 -0
- package/skills/ownership-governance/SKILL.md +104 -0
- package/skills/pause-mission/README.md +18 -0
- package/skills/pause-mission/SKILL.md +112 -0
- package/skills/place-node/README.md +12 -0
- package/skills/place-node/SKILL.md +47 -0
- package/skills/place-node/scripts/place-node.mts +157 -0
- package/skills/plan-retirement/README.md +32 -0
- package/skills/plan-retirement/SKILL.md +90 -0
- package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
- package/skills/plugin-contract-governance/README.md +12 -0
- package/skills/plugin-contract-governance/SKILL.md +112 -0
- package/skills/remediation-governance/README.md +46 -0
- package/skills/remediation-governance/SKILL.md +78 -0
- package/skills/resolve-governances/README.md +18 -0
- package/skills/resolve-governances/SKILL.md +50 -0
- package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
- package/skills/resolve-tracking/SKILL.md +64 -0
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
- package/skills/resume-mission/README.md +12 -0
- package/skills/resume-mission/SKILL.md +53 -0
- package/skills/scaffold-project-spec/README.md +7 -0
- package/skills/scaffold-project-spec/SKILL.md +192 -0
- package/skills/sdd/README.md +7 -0
- package/skills/sdd/SKILL.md +92 -0
- package/skills/solution-producer-governance/README.md +9 -0
- package/skills/solution-producer-governance/SKILL.md +44 -0
- package/skills/spec-format-governance/README.md +73 -0
- package/skills/spec-format-governance/SKILL.md +114 -0
- package/skills/spec-gate/README.md +26 -0
- package/skills/spec-gate/SKILL.md +201 -0
- package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
- package/skills/spec-gate/scripts/check-suite.mts +501 -0
- package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
- package/skills/spec-producer-governance/README.md +7 -0
- package/skills/spec-producer-governance/SKILL.md +86 -0
- package/skills/spec-structure-governance/README.md +40 -0
- package/skills/spec-structure-governance/SKILL.md +169 -0
- package/skills/ssa-lowering/README.md +26 -0
- package/skills/ssa-lowering/SKILL.md +181 -0
- package/skills/start-mission/README.md +7 -0
- package/skills/start-mission/SKILL.md +115 -0
- package/skills/suite-format-governance/README.md +75 -0
- package/skills/suite-format-governance/SKILL.md +299 -0
- package/skills/suite-format-governance/references/rubric.md +313 -0
- package/skills/touch-set-correction/README.md +16 -0
- package/skills/touch-set-correction/SKILL.md +67 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
- package/skills/verify-scenarios/README.md +17 -0
- package/skills/verify-scenarios/SKILL.md +109 -0
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +386 -0
|
@@ -0,0 +1,212 @@
|
|
|
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 } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
|
|
24
|
+
export const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed'])
|
|
25
|
+
|
|
26
|
+
/** The default mission dispatch status — the meaning of an absent top-level `status`. */
|
|
27
|
+
export const DEFAULT_PLAN_STATUS = 'active'
|
|
28
|
+
|
|
29
|
+
export interface PlanRecord {
|
|
30
|
+
/** The CR ref — the plan's filename without the `.plan.md` suffix. */
|
|
31
|
+
cr: string
|
|
32
|
+
/** Frontmatter `name`, or '' when absent. */
|
|
33
|
+
name: string
|
|
34
|
+
/** Total todos in the brief. */
|
|
35
|
+
total: number
|
|
36
|
+
/** Todos with status `completed`. */
|
|
37
|
+
completed: number
|
|
38
|
+
/** Todos with status `in_progress`. */
|
|
39
|
+
inProgress: number
|
|
40
|
+
/** The mission dispatch flag — the top-level `status` (`active` when unset). */
|
|
41
|
+
status: string
|
|
42
|
+
/** The lead line of the `## NEXT` resume anchor, or '' when there is none. */
|
|
43
|
+
next: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PlanFrontmatter {
|
|
47
|
+
name: string
|
|
48
|
+
/** The mission dispatch flag — the top-level `status` (`active` when unset). */
|
|
49
|
+
status: string
|
|
50
|
+
total: number
|
|
51
|
+
completed: number
|
|
52
|
+
inProgress: number
|
|
53
|
+
pending: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Frontmatter parse (a minimal YAML subset — only the plan-brief schema) ──
|
|
57
|
+
// Extracts the leading `---` … `---` block and reads `name` plus the `todos:` list's tally
|
|
58
|
+
// by `status`. Returns null when there is no frontmatter block at all (a stray file).
|
|
59
|
+
export function parsePlanFrontmatter(text: string): PlanFrontmatter | null {
|
|
60
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text)
|
|
61
|
+
if (!m) return null
|
|
62
|
+
const fm: PlanFrontmatter = {
|
|
63
|
+
name: '',
|
|
64
|
+
status: DEFAULT_PLAN_STATUS,
|
|
65
|
+
total: 0,
|
|
66
|
+
completed: 0,
|
|
67
|
+
inProgress: 0,
|
|
68
|
+
pending: 0,
|
|
69
|
+
}
|
|
70
|
+
let inTodos = false // currently inside the top-level `todos:` block
|
|
71
|
+
for (const raw of m[1].split('\n')) {
|
|
72
|
+
const line = raw.replace(/\r$/, '')
|
|
73
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue
|
|
74
|
+
const indent = line.length - line.trimStart().length
|
|
75
|
+
const trimmed = line.trim()
|
|
76
|
+
if (indent === 0) {
|
|
77
|
+
inTodos = false
|
|
78
|
+
const [key, ...rest] = trimmed.split(':')
|
|
79
|
+
const value = rest.join(':').trim()
|
|
80
|
+
if (key === 'name') fm.name = unquote(value)
|
|
81
|
+
// The top-level `status` is the plan's dispatch flag (distinct from a todo's
|
|
82
|
+
// `status`, which sits indented inside `todos:`). An empty value stays the default.
|
|
83
|
+
else if (key === 'status') {
|
|
84
|
+
const v = unquote(value)
|
|
85
|
+
if (v !== '') fm.status = v
|
|
86
|
+
} else if (key === 'todos') inTodos = true // value is empty; the list follows, indented
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
// Inside the todos list — count each item's status. A list item's keys sit at
|
|
90
|
+
// indent ≥ 2 (`- id:` / `status:`); the status line is `status: <value>`.
|
|
91
|
+
if (inTodos) {
|
|
92
|
+
const sm = /^(?:-\s+)?status:\s*(.+)$/.exec(trimmed)
|
|
93
|
+
if (sm) {
|
|
94
|
+
const status = unquote(sm[1].trim())
|
|
95
|
+
fm.total++
|
|
96
|
+
if (status === 'completed') fm.completed++
|
|
97
|
+
else if (status === 'in_progress') fm.inProgress++
|
|
98
|
+
else if (status === 'pending') fm.pending++
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return fm
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function unquote(v: string): string {
|
|
106
|
+
return v.replace(/^["']|["']$/g, '')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── The `## NEXT` resume anchor lead ──
|
|
110
|
+
// Find the `## NEXT` section (the resume anchor pause-mission writes) and return its first
|
|
111
|
+
// content line — the next concrete action — trimmed of markdown bullet/emphasis markers and
|
|
112
|
+
// capped. Returns '' when there is no NEXT section or it is empty. Body read is bounded to
|
|
113
|
+
// this one section; the rest of the body is never parsed.
|
|
114
|
+
export function nextLead(text: string): string {
|
|
115
|
+
const lines = text.split('\n')
|
|
116
|
+
let i = lines.findIndex((l) => /^##\s+NEXT\b/i.test(l.trim()))
|
|
117
|
+
if (i === -1) return ''
|
|
118
|
+
for (i += 1; i < lines.length; i++) {
|
|
119
|
+
const l = lines[i].replace(/\r$/, '').trim()
|
|
120
|
+
if (l === '') continue
|
|
121
|
+
if (l.startsWith('#')) return '' // hit the next heading with no content between
|
|
122
|
+
const cleaned = l
|
|
123
|
+
.replace(/^[-*]\s+/, '')
|
|
124
|
+
.replace(/\*\*/g, '')
|
|
125
|
+
.trim()
|
|
126
|
+
return cleaned.length > 200 ? `${cleaned.slice(0, 197)}...` : cleaned
|
|
127
|
+
}
|
|
128
|
+
return ''
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Scan ──
|
|
132
|
+
// List the plan-brief filenames (relative names) directly under <root>/.agents/plans. Only
|
|
133
|
+
// files ending `.plan.md` are briefs; a missing plans dir yields the empty set.
|
|
134
|
+
function discoverPlanFiles(root: string): string[] {
|
|
135
|
+
const dir = join(root, '.agents', 'plans')
|
|
136
|
+
if (!existsSync(dir)) return []
|
|
137
|
+
let entries: import('node:fs').Dirent[]
|
|
138
|
+
try {
|
|
139
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
140
|
+
} catch {
|
|
141
|
+
return []
|
|
142
|
+
}
|
|
143
|
+
return entries
|
|
144
|
+
.filter((e) => e.isFile() && e.name.endsWith('.plan.md'))
|
|
145
|
+
.map((e) => e.name)
|
|
146
|
+
.sort()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Collect ──
|
|
150
|
+
// The list of resumable missions: every plan brief carrying a frontmatter block, keyed by
|
|
151
|
+
// its CR ref, with its todo tally and NEXT lead. Sorted by cr ref for stable output.
|
|
152
|
+
export function collectPlans(root: string): PlanRecord[] {
|
|
153
|
+
const out: PlanRecord[] = []
|
|
154
|
+
for (const name of discoverPlanFiles(root)) {
|
|
155
|
+
let text: string
|
|
156
|
+
try {
|
|
157
|
+
text = readFileSync(join(root, '.agents', 'plans', name), 'utf8')
|
|
158
|
+
} catch {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
const fm = parsePlanFrontmatter(text)
|
|
162
|
+
if (!fm) continue // no frontmatter — a stray, not a brief
|
|
163
|
+
out.push({
|
|
164
|
+
cr: name.replace(/\.plan\.md$/, ''),
|
|
165
|
+
name: fm.name,
|
|
166
|
+
total: fm.total,
|
|
167
|
+
completed: fm.completed,
|
|
168
|
+
inProgress: fm.inProgress,
|
|
169
|
+
status: fm.status,
|
|
170
|
+
next: nextLead(text),
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
return out.sort((a, b) => (a.cr < b.cr ? -1 : a.cr > b.cr ? 1 : 0))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Filter ──
|
|
177
|
+
// Narrow a plan set to one dispatch status — the opt-in selector the gateway's dispatch loop
|
|
178
|
+
// uses to build the approved queue. Records already carry a concrete `status` (unset → the
|
|
179
|
+
// default `active`), so a filter to `active` includes the unset briefs and a value no brief
|
|
180
|
+
// carries (an off-enum or simply-absent status) yields the empty set.
|
|
181
|
+
export function filterByStatus(plans: PlanRecord[], status: string): PlanRecord[] {
|
|
182
|
+
return plans.filter((p) => p.status === status)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Output ──
|
|
186
|
+
const COLUMNS = ['cr', 'name', 'total', 'completed', 'inProgress', 'status', 'next'] as const
|
|
187
|
+
|
|
188
|
+
// Quote a TOON field only when it carries the delimiter, a quote, or edge whitespace.
|
|
189
|
+
function toonField(v: string): string {
|
|
190
|
+
if (v === '' || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
|
|
191
|
+
return v
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function toToon(plans: PlanRecord[]): string {
|
|
195
|
+
const header = `plans[${plans.length}]{${COLUMNS.join(',')}}:`
|
|
196
|
+
const rows = plans.map((p) => ` ${COLUMNS.map((c) => toonField(String(p[c]))).join(',')}`)
|
|
197
|
+
return [header, ...rows].join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function main(argv: string[]): number {
|
|
201
|
+
const root = argv.includes('--root') ? (argv[argv.indexOf('--root') + 1] ?? '.') : '.'
|
|
202
|
+
const format = argv.includes('--format') ? argv[argv.indexOf('--format') + 1] : 'toon'
|
|
203
|
+
const statusFilter = argv.includes('--status') ? argv[argv.indexOf('--status') + 1] : undefined
|
|
204
|
+
let plans = collectPlans(root)
|
|
205
|
+
// Opt-in: `--status <value>` narrows to the dispatch queue; absent, no status filter is applied.
|
|
206
|
+
if (statusFilter !== undefined) plans = filterByStatus(plans, statusFilter)
|
|
207
|
+
const out = format === 'json' ? JSON.stringify(plans, null, 2) : toToon(plans)
|
|
208
|
+
process.stdout.write(`${out}\n`)
|
|
209
|
+
return 0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (import.meta.main) process.exit(main(process.argv.slice(2)))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# discover-specs
|
|
2
|
+
|
|
3
|
+
The concrete engine for SDD **spec discovery**. A non-user-invocable skill carrying a self-contained `.mts`
|
|
4
|
+
script that scans the three fixed SDD spec locations — plus any opt-in extra anchors declared in
|
|
5
|
+
`.agents/sdd/spec-anchors.toml` (ADR-0019, curated via `manage-spec-anchors`) — filters candidates by
|
|
6
|
+
the lifecycle `status` shape, parses each `spec.md`'s frontmatter only, and emits a TOON list of the
|
|
7
|
+
specs found.
|
|
8
|
+
|
|
9
|
+
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
+
- **Script:** [`scripts/discover-specs.mts`](./scripts/discover-specs.mts)
|
|
11
|
+
- **Tests:** [`scripts/discover-specs.test.mts`](./scripts/discover-specs.test.mts) (`node:test`)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node scripts/discover-specs.mts --root . --format toon
|
|
15
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: discover-specs
|
|
3
|
+
description: "Partial Skill: invoke by name only — corpus/discovery's frontmatter-scanning engine across the SDD spec locations — used by the sdd gateway to scan statuses and by start-mission to locate the project spec, not triggered by users directly."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
internal: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Discover Specs
|
|
10
|
+
|
|
11
|
+
The concrete engine for SDD **spec discovery**. It locates the
|
|
12
|
+
project specs in a repo and returns each one's frontmatter — **without reading any spec body** — so a
|
|
13
|
+
consumer (the **gateway**, corpus tooling) can route on `status` / `project-path` / `approval`
|
|
14
|
+
cheaply. It carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention).
|
|
15
|
+
|
|
16
|
+
## Recognition — location-bounded and shape-confirmed
|
|
17
|
+
|
|
18
|
+
A `spec.md` is a spec only when **both** hold (ADR-0017, narrowed; extra anchors per ADR-0019 —
|
|
19
|
+
`sdd:lifecycle-governance`):
|
|
20
|
+
|
|
21
|
+
- **Location** — it sits at one of the three fixed SDD spec locations, **or** at an extra anchor the
|
|
22
|
+
project declared in `.agents/sdd/spec-anchors.toml`:
|
|
23
|
+
1. `.agents/spec/spec.md` — repo-root single-project
|
|
24
|
+
2. `.agents/specs/<project>/spec.md` — repo-root multi-project
|
|
25
|
+
3. `<project-path>/.agents/spec/spec.md` — a nested project (the `**` is the project-path, any depth)
|
|
26
|
+
4. **extra anchors** — each config entry, a repo-relative pattern (`*` globs one segment, `**`
|
|
27
|
+
globs zero or more segments at any depth, `<project>` globs and captures a name); **opt-in and
|
|
28
|
+
additive** (absent config ⇒ only 1–3, so today's behavior is unchanged). Curated via the
|
|
29
|
+
`manage-spec-anchors` skill.
|
|
30
|
+
- **Shape** — its frontmatter `status` is in the lifecycle enum (`draft | approved | implemented |
|
|
31
|
+
deprecated`). A `spec.md` at any recognized location with **no** lifecycle `status` is skipped (so
|
|
32
|
+
the scan never grabs a stray file by accident); a status-bearing `spec.md` at neither a fixed
|
|
33
|
+
convention nor a declared extra anchor is not discovered. An **unreadable or malformed**
|
|
34
|
+
`spec-anchors.toml` is ignored (warn + fall back to the fixed conventions), so the scan never
|
|
35
|
+
crashes on a corrupt config.
|
|
36
|
+
|
|
37
|
+
## Run the scan
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node "<skill>/scripts/discover-specs.mts" [--root .] [--format toon|json] [--resolve <name>]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Default `--root` is the current directory; default `--format` is **TOON** (the token-efficient
|
|
44
|
+
tabular form the gateway scans).
|
|
45
|
+
- Emits one row per spec, sorted by folder slug, with columns
|
|
46
|
+
`path,name,nameSource,status,projectPath,approvals` — `path` is the spec's root-relative folder
|
|
47
|
+
slug; `name` is the project name and `nameSource` is `declared | derived | guessed` (below);
|
|
48
|
+
`approvals` is the gate verdicts as `<gate>:<verdict>` pairs joined by `;`.
|
|
49
|
+
- `--resolve <name>` filters to the **exact (case-insensitive) name matches** — 0 rows = none,
|
|
50
|
+
1 = resolved, >1 = ambiguous (the consumer disambiguates with the user).
|
|
51
|
+
- `--format json` emits the same records as a flat JSON array for non-LLM consumers.
|
|
52
|
+
|
|
53
|
+
Example (TOON):
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
specs[2]{path,name,nameSource,status,projectPath,approvals}:
|
|
57
|
+
.agents/specs/aced,aced,derived,implemented,plugins/aced,spec:approve;impl:approve
|
|
58
|
+
.agents/specs/sdd,sdd,derived,approved,plugins/sdd,spec:approve
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**`name-source`** flags how trustworthy the name is: **`declared`** (frontmatter `name`,
|
|
62
|
+
authoritative), **`derived`** (repo-root single-project → `repo`; a `.agents/specs/<project>` folder
|
|
63
|
+
names itself), **`guessed`** (a nested project's folder basename — confirm with the user before
|
|
64
|
+
relying on it).
|
|
65
|
+
|
|
66
|
+
When `node` is absent, an agent performs the same derivation by hand: scan the three spec locations,
|
|
67
|
+
keep each `spec.md` whose frontmatter `status` is in the enum, read its frontmatter only, and derive
|
|
68
|
+
the name per the rules above.
|
|
69
|
+
|
|
70
|
+
## Boundaries
|
|
71
|
+
|
|
72
|
+
Frontmatter only — the script reads the whole file but its **output** carries no body content, so a
|
|
73
|
+
consuming agent never spends tokens on a body (`digest` reads bodies; this never does). It owns no
|
|
74
|
+
lifecycle state and writes nothing. The script resolves a name **deterministically** (exact match →
|
|
75
|
+
spec, or the candidate set); **disambiguating with the user** is the consumer's agentic step, not the
|
|
76
|
+
script's.
|