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,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// manage-ignore — the concrete curation engine for `.agents/sdd/.sddignore`, the optional
|
|
3
|
+
// gitignore-syntax file `resolve-tracking` consults to decide whether an artifact is tracked or
|
|
4
|
+
// ignored. It reads/writes ONLY `.agents/sdd/.sddignore` — never a spec.md, status, approval, or
|
|
5
|
+
// freeze.
|
|
6
|
+
//
|
|
7
|
+
// Operations:
|
|
8
|
+
// --list print every rule in file order (a missing file prints nothing)
|
|
9
|
+
// --add <pattern> validate + append a well-formed gitignore rule (creates the file)
|
|
10
|
+
// --remove <pattern> drop a matching rule, preserving the order of the rest (absent = no-op)
|
|
11
|
+
// --induce <path> offer a literal-path candidate + a ** generalization (persists nothing)
|
|
12
|
+
// --preview <pattern> list the working-tree paths the pattern would ignore / re-track
|
|
13
|
+
//
|
|
14
|
+
// `.sddignore` is gitignore syntax: a plain pattern => ignored, a leading `!` => re-included, `#`
|
|
15
|
+
// comments and blank lines allowed. Order is MEANINGFUL (last-match-wins), so rules are NEVER
|
|
16
|
+
// re-sorted — add appends, remove keeps the surviving order.
|
|
17
|
+
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps
|
|
18
|
+
// (the repo's node-≥23.6 convention).
|
|
19
|
+
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
21
|
+
import { dirname, join } from 'node:path'
|
|
22
|
+
|
|
23
|
+
const IGNORE_FILE = '.agents/sdd/.sddignore'
|
|
24
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
|
|
25
|
+
|
|
26
|
+
// ── the ignore file (raw-line preserving so order + comments survive CRUD) ──
|
|
27
|
+
|
|
28
|
+
// Split into lines, dropping the single trailing empty produced by a final newline.
|
|
29
|
+
export function splitLines(text: string): string[] {
|
|
30
|
+
const lines = text.split('\n')
|
|
31
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
|
32
|
+
return lines
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readIgnoreLines(root: string): string[] {
|
|
36
|
+
const file = join(root, IGNORE_FILE)
|
|
37
|
+
if (!existsSync(file)) return []
|
|
38
|
+
try {
|
|
39
|
+
return splitLines(readFileSync(file, 'utf8'))
|
|
40
|
+
} catch {
|
|
41
|
+
return []
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeIgnoreLines(root: string, lines: string[]): void {
|
|
46
|
+
const file = join(root, IGNORE_FILE)
|
|
47
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
48
|
+
writeFileSync(file, lines.length === 0 ? '' : `${lines.join('\n')}\n`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A line is a rule when it is not blank and not a `#` comment. `!re-track` rules are rules.
|
|
52
|
+
export function isRuleLine(line: string): boolean {
|
|
53
|
+
const t = line.trim()
|
|
54
|
+
return t !== '' && !t.startsWith('#')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── list ──
|
|
58
|
+
|
|
59
|
+
// Every rule in file order; comments and blank lines are omitted (a missing file yields []).
|
|
60
|
+
export function listRules(root: string): string[] {
|
|
61
|
+
return readIgnoreLines(root)
|
|
62
|
+
.filter(isRuleLine)
|
|
63
|
+
.map((l) => l.trim())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── validation ──
|
|
67
|
+
|
|
68
|
+
// True when the bracket expression opened at some `[` is never closed by a later `]`.
|
|
69
|
+
function hasUnclosedBracket(s: string): boolean {
|
|
70
|
+
let i = 0
|
|
71
|
+
while (i < s.length) {
|
|
72
|
+
if (s[i] === '\\') {
|
|
73
|
+
i += 2
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (s[i] === '[') {
|
|
77
|
+
const close = s.indexOf(']', i + 1)
|
|
78
|
+
if (close === -1) return true
|
|
79
|
+
i = close + 1
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
i++
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A well-formed gitignore pattern: non-empty after stripping a leading `!` and surrounding space,
|
|
88
|
+
// not a comment, and with no unclosed `[…]` bracket expression.
|
|
89
|
+
export function isValidPattern(pattern: string): boolean {
|
|
90
|
+
let s = pattern
|
|
91
|
+
if (s.startsWith('!')) s = s.slice(1)
|
|
92
|
+
s = s.replace(/^\s+|\s+$/g, '')
|
|
93
|
+
if (s === '' || s.startsWith('#')) return false
|
|
94
|
+
if (hasUnclosedBracket(s)) return false
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── add / remove (CRUD, order-preserving) ──
|
|
99
|
+
|
|
100
|
+
export type CrudResult =
|
|
101
|
+
| { ok: true; changed: boolean; rules: string[]; message: string }
|
|
102
|
+
| { ok: false; reason: string }
|
|
103
|
+
|
|
104
|
+
// Append a well-formed rule after the existing lines (creating the file when absent). Existing
|
|
105
|
+
// order is preserved; a duplicate is a no-op.
|
|
106
|
+
export function addRule(root: string, pattern: string): CrudResult {
|
|
107
|
+
const p = pattern.trim()
|
|
108
|
+
if (!isValidPattern(p)) return { ok: false, reason: 'malformed gitignore pattern' }
|
|
109
|
+
const lines = readIgnoreLines(root)
|
|
110
|
+
if (lines.some((l) => l.trim() === p)) {
|
|
111
|
+
return { ok: true, changed: false, rules: listRules(root), message: 'already present' }
|
|
112
|
+
}
|
|
113
|
+
const next = [...lines, p]
|
|
114
|
+
writeIgnoreLines(root, next)
|
|
115
|
+
return { ok: true, changed: true, rules: next.filter(isRuleLine).map((l) => l.trim()), message: `added ${p}` }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Drop every line whose rule text equals the pattern, keeping the order of the rest. Removing an
|
|
119
|
+
// absent rule leaves the file byte-for-byte unchanged.
|
|
120
|
+
export function removeRule(root: string, pattern: string): CrudResult {
|
|
121
|
+
const p = pattern.trim()
|
|
122
|
+
const lines = readIgnoreLines(root)
|
|
123
|
+
const next = lines.filter((l) => l.trim() !== p)
|
|
124
|
+
if (next.length === lines.length) {
|
|
125
|
+
return { ok: true, changed: false, rules: listRules(root), message: 'nothing was removed' }
|
|
126
|
+
}
|
|
127
|
+
writeIgnoreLines(root, next)
|
|
128
|
+
return { ok: true, changed: true, rules: next.filter(isRuleLine).map((l) => l.trim()), message: `removed ${p}` }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── induce ──
|
|
132
|
+
|
|
133
|
+
export type InduceResult = { ok: true; candidates: string[] } | { ok: false; reason: string }
|
|
134
|
+
|
|
135
|
+
// From a sample repo-relative path, offer a literal-path candidate and a `**` generalization that
|
|
136
|
+
// ignores the same basename anywhere in the tree. Persists nothing; refuses a path not under the
|
|
137
|
+
// repo (leading slash or a `..` escape).
|
|
138
|
+
export function inducePatterns(_root: string, samplePath: string): InduceResult {
|
|
139
|
+
const normalized = samplePath.trim().replace(/\\/g, '/')
|
|
140
|
+
if (normalized.startsWith('/')) return { ok: false, reason: 'path is not repo-relative (absolute)' }
|
|
141
|
+
const rel = normalized.replace(/\/+$/g, '')
|
|
142
|
+
if (rel === '' || /(^|\/)\.\.(\/|$)/.test(rel)) {
|
|
143
|
+
return { ok: false, reason: 'path is not repo-relative' }
|
|
144
|
+
}
|
|
145
|
+
const base = rel.split('/').pop() ?? rel
|
|
146
|
+
const generalized = `**/${base}`
|
|
147
|
+
const candidates = generalized === rel ? [rel] : [rel, generalized]
|
|
148
|
+
return { ok: true, candidates }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── preview ──
|
|
152
|
+
|
|
153
|
+
export type PreviewResult = { ok: true; negated: boolean; matches: string[] } | { ok: false; reason: string }
|
|
154
|
+
|
|
155
|
+
// Compile one gitignore pattern (already `!`-stripped) to a full-match RegExp. `**` spans
|
|
156
|
+
// separators, `*`/`?` stay within one segment, a leading `/` (or an internal `/`) anchors to the
|
|
157
|
+
// repo root, a trailing `/` marks a dir. Every match also covers the node's descendants.
|
|
158
|
+
export function gitignoreToRegExp(pattern: string): RegExp {
|
|
159
|
+
let pat = pattern
|
|
160
|
+
if (pat.endsWith('/')) pat = pat.slice(0, -1) // trailing-slash dir marker
|
|
161
|
+
let anchored = pat.startsWith('/')
|
|
162
|
+
if (anchored) pat = pat.slice(1)
|
|
163
|
+
if (pat.includes('/')) anchored = true // an internal separator anchors to root
|
|
164
|
+
let body = ''
|
|
165
|
+
for (let i = 0; i < pat.length; i++) {
|
|
166
|
+
const c = pat[i]
|
|
167
|
+
if (c === '*') {
|
|
168
|
+
if (pat[i + 1] === '*') {
|
|
169
|
+
i++
|
|
170
|
+
if (pat[i + 1] === '/') {
|
|
171
|
+
i++
|
|
172
|
+
body += '(?:.*/)?' // `**/` spans zero or more directory segments
|
|
173
|
+
} else {
|
|
174
|
+
body += '.*' // `**` spans separators
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
body += '[^/]*' // `*` stays within a segment
|
|
178
|
+
}
|
|
179
|
+
} else if (c === '?') {
|
|
180
|
+
body += '[^/]'
|
|
181
|
+
} else if (c === '[') {
|
|
182
|
+
let j = i + 1
|
|
183
|
+
let cls = '['
|
|
184
|
+
if (pat[j] === '!') {
|
|
185
|
+
cls += '^'
|
|
186
|
+
j++
|
|
187
|
+
}
|
|
188
|
+
while (j < pat.length && pat[j] !== ']') {
|
|
189
|
+
cls += /[\\^\]]/.test(pat[j]) ? `\\${pat[j]}` : pat[j]
|
|
190
|
+
j++
|
|
191
|
+
}
|
|
192
|
+
cls += ']'
|
|
193
|
+
body += cls
|
|
194
|
+
i = j
|
|
195
|
+
} else {
|
|
196
|
+
body += c.replace(/[.+^${}()|\\]/g, '\\$&')
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const prefix = anchored ? '^' : '^(?:.*/)?' // unanchored patterns match at any depth
|
|
200
|
+
return new RegExp(`${prefix}${body}(?:/.*)?$`) // also cover everything below the matched node
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function matchesPattern(pattern: string, path: string): boolean {
|
|
204
|
+
const body = pattern.startsWith('!') ? pattern.slice(1) : pattern
|
|
205
|
+
return gitignoreToRegExp(body).test(path)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Every repo-relative path (files and dirs) reachable from the root, skipping SKIP_DIRS.
|
|
209
|
+
function walkTree(root: string): string[] {
|
|
210
|
+
const out: string[] = []
|
|
211
|
+
const walk = (rel: string): void => {
|
|
212
|
+
let entries: import('node:fs').Dirent[]
|
|
213
|
+
try {
|
|
214
|
+
entries = readdirSync(join(root, rel), { withFileTypes: true })
|
|
215
|
+
} catch {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
for (const e of entries) {
|
|
219
|
+
if (SKIP_DIRS.has(e.name)) continue
|
|
220
|
+
const child = rel ? `${rel}/${e.name}` : e.name
|
|
221
|
+
out.push(child)
|
|
222
|
+
if (e.isDirectory()) walk(child)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
walk('')
|
|
226
|
+
return out.sort()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// List the working-tree paths a candidate pattern would ignore. For a `!pattern`, the same paths
|
|
230
|
+
// are the ones it would RE-TRACK (re-include). Persists nothing; a malformed pattern is refused.
|
|
231
|
+
export function previewPattern(root: string, pattern: string): PreviewResult {
|
|
232
|
+
const p = pattern.trim()
|
|
233
|
+
if (!isValidPattern(p)) return { ok: false, reason: 'malformed gitignore pattern' }
|
|
234
|
+
const negated = p.startsWith('!')
|
|
235
|
+
const matches = walkTree(root).filter((path) => matchesPattern(p, path))
|
|
236
|
+
return { ok: true, negated, matches }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── CLI ──
|
|
240
|
+
|
|
241
|
+
function flag(argv: string[], name: string): string | undefined {
|
|
242
|
+
const i = argv.indexOf(name)
|
|
243
|
+
return i === -1 ? undefined : argv[i + 1]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function main(argv: string[]): number {
|
|
247
|
+
const root = flag(argv, '--root') ?? '.'
|
|
248
|
+
const w = (s: string) => process.stdout.write(`${s}\n`)
|
|
249
|
+
|
|
250
|
+
if (argv.includes('--list')) {
|
|
251
|
+
for (const rule of listRules(root)) w(rule)
|
|
252
|
+
return 0
|
|
253
|
+
}
|
|
254
|
+
if (argv.includes('--add')) {
|
|
255
|
+
const r = addRule(root, flag(argv, '--add') ?? '')
|
|
256
|
+
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
257
|
+
return r.ok ? 0 : 1
|
|
258
|
+
}
|
|
259
|
+
if (argv.includes('--remove')) {
|
|
260
|
+
const r = removeRule(root, flag(argv, '--remove') ?? '')
|
|
261
|
+
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
262
|
+
return r.ok ? 0 : 1
|
|
263
|
+
}
|
|
264
|
+
if (argv.includes('--induce')) {
|
|
265
|
+
const r = inducePatterns(root, flag(argv, '--induce') ?? '')
|
|
266
|
+
if (!r.ok) {
|
|
267
|
+
w(`unusable: ${r.reason}`)
|
|
268
|
+
return 1
|
|
269
|
+
}
|
|
270
|
+
w('Candidate ignore patterns:')
|
|
271
|
+
for (const c of r.candidates) w(` ${c}`)
|
|
272
|
+
return 0
|
|
273
|
+
}
|
|
274
|
+
if (argv.includes('--preview')) {
|
|
275
|
+
const r = previewPattern(root, flag(argv, '--preview') ?? '')
|
|
276
|
+
if (!r.ok) {
|
|
277
|
+
w(`invalid: ${r.reason}`)
|
|
278
|
+
return 1
|
|
279
|
+
}
|
|
280
|
+
const verb = r.negated ? 're-track' : 'ignore'
|
|
281
|
+
if (r.matches.length === 0) {
|
|
282
|
+
w(`This pattern would ${verb} no path.`)
|
|
283
|
+
return 0
|
|
284
|
+
}
|
|
285
|
+
w(`This pattern would ${verb}:`)
|
|
286
|
+
for (const m of r.matches) w(` ${m}`)
|
|
287
|
+
return 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
w('usage: manage-ignore --list | --add <p> | --remove <p> | --induce <path> | --preview <p>')
|
|
291
|
+
return 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (import.meta.main) process.exit(main(process.argv.slice(2)))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# manage-scenario-bridge
|
|
2
|
+
|
|
3
|
+
The concrete engine for `<project-path>/.agents/sdd/scenario-bridge.toml` — the one-time
|
|
4
|
+
per-project wiring the `verify-scenarios` bridge and the impl-judge step-0 consumption both read. A
|
|
5
|
+
non-user-invocable skill, loaded in-session by the `manage` gateway (Setup & discovery), carrying a
|
|
6
|
+
self-contained `.mts` script that lists / scaffolds / adds sources so users never hand-author the
|
|
7
|
+
config.
|
|
8
|
+
|
|
9
|
+
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
+
- **Script:** [`scripts/manage-scenario-bridge.mts`](./scripts/manage-scenario-bridge.mts)
|
|
11
|
+
- **Tests:** [`scripts/manage-scenario-bridge.test.mts`](./scripts/manage-scenario-bridge.test.mts) (`node:test`)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node scripts/manage-scenario-bridge.mts --project-path packages/cyberlegion --list
|
|
15
|
+
node scripts/manage-scenario-bridge.mts --project-path packages/cyberlegion --scaffold \
|
|
16
|
+
--adapter junit --command "vitest run --reporter=junit --outputFile=.agents/.scenario-report.xml" \
|
|
17
|
+
--report-path .agents/.scenario-report.xml
|
|
18
|
+
node scripts/manage-scenario-bridge.mts --project-path packages/cyberlegion --add \
|
|
19
|
+
--adapter junit --report-path .agents/.other-report.xml
|
|
20
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-scenario-bridge
|
|
3
|
+
description: "Partial Skill: invoke by name only — the curation engine for a project's scenario-bridge.toml — loaded by the manage gateway (Setup & discovery), not triggered by users directly."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
internal: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Manage Scenario Bridge
|
|
10
|
+
|
|
11
|
+
The concrete engine for `<project-path>/.agents/sdd/scenario-bridge.toml` — the one-time
|
|
12
|
+
per-project wiring the [`verify-scenarios`](../verify-scenarios/) bridge and the `sdd-impl-judge`
|
|
13
|
+
step-0 consumption both read. It is the **write** side of that config (`verify-scenarios` is the
|
|
14
|
+
read side). It exists so a user wires a project's scenario bridge through a clean interface instead
|
|
15
|
+
of hand-authoring the TOML. Loaded **in-session** by the `manage` gateway (Setup & discovery group);
|
|
16
|
+
it carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention).
|
|
17
|
+
|
|
18
|
+
## The file it curates
|
|
19
|
+
|
|
20
|
+
`<project-path>/.agents/sdd/scenario-bridge.toml` is an array-of-tables of result **sources**,
|
|
21
|
+
mirroring `verify-scenarios`' own config shape — never diverge the format:
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
[[source]]
|
|
25
|
+
adapter = "junit"
|
|
26
|
+
command = "pnpm build && vitest run src --reporter=junit --outputFile=.agents/.scenario-report.xml"
|
|
27
|
+
reportPath = ".agents/.scenario-report.xml"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A colocated project's `project-path` is its own repo root, so the config lands at the familiar
|
|
31
|
+
`.agents/sdd/scenario-bridge.toml` — no behavior change for a single-project repo. A monorepo
|
|
32
|
+
member's config sits at `<project-path>/.agents/sdd/scenario-bridge.toml`, beside the code and
|
|
33
|
+
reports it covers.
|
|
34
|
+
|
|
35
|
+
## Run an operation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node "<skill>/scripts/manage-scenario-bridge.mts" --project-path <dir> <operation>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Operation | Effect |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `--list` | print every configured `[[source]]` block in order; a missing file lists nothing, no error |
|
|
44
|
+
| `--scaffold --adapter <a> [--command <c>] --report-path <p>` | create the config under the project's `project-path` with one source block; refused when the file already exists |
|
|
45
|
+
| `--add --adapter <a> [--command <c>] --report-path <p>` | append a source block to an existing config; refused when the file does not exist (names `--scaffold` as the entry point) |
|
|
46
|
+
|
|
47
|
+
A `--scaffold` or `--add` missing `--adapter` or `--report-path` is refused — no source block is
|
|
48
|
+
written.
|
|
49
|
+
|
|
50
|
+
## Boundaries
|
|
51
|
+
|
|
52
|
+
Writes **only** `<project-path>/.agents/sdd/scenario-bridge.toml` — never a `spec.md`, `status`,
|
|
53
|
+
`approval`, or a freeze; it is operational config, not spec content (so `manage`'s write-ownership
|
|
54
|
+
guard holds). It does not author the binding tests a source reports on (the impl-producer does,
|
|
55
|
+
`sdd:impl-producer-governance`), and does not run the bridge or judge anything (`verify-scenarios`
|
|
56
|
+
and the impl-judge do).
|
|
57
|
+
|
|
58
|
+
When `node` is absent, an agent performs the same edits by hand: read the file, apply the
|
|
59
|
+
list/scaffold/add, and refuse a scaffold over an existing file or a request missing a required
|
|
60
|
+
field.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// manage-scenario-bridge — the concrete curation engine for `<project-path>/.agents/sdd/scenario-bridge.toml`,
|
|
3
|
+
// the one-time per-project wiring the verify-scenarios bridge and the impl-judge step-0 consumption
|
|
4
|
+
// both read. It reads/writes ONLY that one project's scenario-bridge.toml — never a spec.md, status,
|
|
5
|
+
// approval, or a freeze; it never runs the bridge or a test.
|
|
6
|
+
//
|
|
7
|
+
// Operations:
|
|
8
|
+
// --list print every configured [[source]] block in order
|
|
9
|
+
// --scaffold --adapter <a> [--command <c>] --report-path <p>
|
|
10
|
+
// create the config with one source block; refused
|
|
11
|
+
// when the file already exists
|
|
12
|
+
// --add --adapter <a> [--command <c>] --report-path <p>
|
|
13
|
+
// append a source block to an existing config;
|
|
14
|
+
// refused when the file does not exist
|
|
15
|
+
//
|
|
16
|
+
// Every read/write is resolved beneath --project-path (a colocated project's project-path is its own
|
|
17
|
+
// repo root, so the config lands at the familiar .agents/sdd/scenario-bridge.toml there).
|
|
18
|
+
//
|
|
19
|
+
// Reuses the verify-scenarios bridge's own SourceConfig/parseSourcesToml shape — never diverge the
|
|
20
|
+
// format the reader (verify-scenarios) and the writer (this engine) agree on.
|
|
21
|
+
//
|
|
22
|
+
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps (the
|
|
23
|
+
// repo's node-≥23.6 convention).
|
|
24
|
+
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
26
|
+
import { dirname, join } from 'node:path'
|
|
27
|
+
import { parseSourcesToml, type SourceConfig } from '../../verify-scenarios/scripts/verify-scenarios.mts'
|
|
28
|
+
|
|
29
|
+
export type { SourceConfig }
|
|
30
|
+
|
|
31
|
+
const CONFIG_REL_PATH = '.agents/sdd/scenario-bridge.toml'
|
|
32
|
+
|
|
33
|
+
export function configPath(projectPath: string): string {
|
|
34
|
+
return join(projectPath, CONFIG_REL_PATH)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── list ──
|
|
38
|
+
|
|
39
|
+
// Every configured source in file order; a missing config lists nothing without error.
|
|
40
|
+
export function listSources(projectPath: string): SourceConfig[] {
|
|
41
|
+
const file = configPath(projectPath)
|
|
42
|
+
if (!existsSync(file)) return []
|
|
43
|
+
try {
|
|
44
|
+
return parseSourcesToml(readFileSync(file, 'utf8'))
|
|
45
|
+
} catch {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── render one [[source]] block ──
|
|
51
|
+
|
|
52
|
+
function renderBlock(source: { adapter: string; command?: string; reportPath: string }): string {
|
|
53
|
+
const lines = [
|
|
54
|
+
'[[source]]',
|
|
55
|
+
`adapter = "${source.adapter}"`,
|
|
56
|
+
...(source.command !== undefined ? [`command = "${source.command}"`] : []),
|
|
57
|
+
`reportPath = "${source.reportPath}"`,
|
|
58
|
+
]
|
|
59
|
+
return lines.join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── scaffold / add ──
|
|
63
|
+
|
|
64
|
+
export interface NewSourceInput {
|
|
65
|
+
adapter?: string
|
|
66
|
+
command?: string
|
|
67
|
+
reportPath?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type MutateResult = { ok: true; message: string; sources: SourceConfig[] } | { ok: false; reason: string }
|
|
71
|
+
|
|
72
|
+
function validateInput(
|
|
73
|
+
input: NewSourceInput,
|
|
74
|
+
): { ok: true; adapter: string; command?: string; reportPath: string } | { ok: false; reason: string } {
|
|
75
|
+
if (!input.adapter || !input.reportPath) {
|
|
76
|
+
return { ok: false, reason: 'a source needs both adapter and reportPath' }
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, adapter: input.adapter, command: input.command, reportPath: input.reportPath }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Creates <project-path>/.agents/sdd/scenario-bridge.toml with one [[source]] block. Refuses when the
|
|
82
|
+
// file already exists (use `add` instead) or when adapter/reportPath is missing — no source block is
|
|
83
|
+
// written in the refused case.
|
|
84
|
+
export function scaffoldSource(projectPath: string, input: NewSourceInput): MutateResult {
|
|
85
|
+
const valid = validateInput(input)
|
|
86
|
+
if (!valid.ok) return valid
|
|
87
|
+
const file = configPath(projectPath)
|
|
88
|
+
if (existsSync(file)) {
|
|
89
|
+
return { ok: false, reason: 'scenario-bridge.toml already exists; use add to append a source' }
|
|
90
|
+
}
|
|
91
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
92
|
+
writeFileSync(file, `${renderBlock(valid)}\n`)
|
|
93
|
+
const sources = listSources(projectPath)
|
|
94
|
+
return { ok: true, message: `scaffolded ${file}`, sources }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Appends a [[source]] block to an existing config, leaving the existing sources untouched. Refuses
|
|
98
|
+
// when the config does not exist yet (names scaffold as the entry point) or when adapter/reportPath
|
|
99
|
+
// is missing.
|
|
100
|
+
export function addSource(projectPath: string, input: NewSourceInput): MutateResult {
|
|
101
|
+
const valid = validateInput(input)
|
|
102
|
+
if (!valid.ok) return valid
|
|
103
|
+
const file = configPath(projectPath)
|
|
104
|
+
if (!existsSync(file)) {
|
|
105
|
+
return { ok: false, reason: 'scenario-bridge.toml does not exist yet; use scaffold to create it first' }
|
|
106
|
+
}
|
|
107
|
+
const existing = readFileSync(file, 'utf8')
|
|
108
|
+
const sep = existing.endsWith('\n') ? '' : '\n'
|
|
109
|
+
writeFileSync(file, `${existing}${sep}\n${renderBlock(valid)}\n`)
|
|
110
|
+
const sources = listSources(projectPath)
|
|
111
|
+
return { ok: true, message: `added a source to ${file}`, sources }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── CLI ──
|
|
115
|
+
|
|
116
|
+
function flag(argv: string[], name: string): string | undefined {
|
|
117
|
+
const i = argv.indexOf(name)
|
|
118
|
+
return i === -1 ? undefined : argv[i + 1]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function inputFromArgv(argv: string[]): NewSourceInput {
|
|
122
|
+
return {
|
|
123
|
+
adapter: flag(argv, '--adapter'),
|
|
124
|
+
command: flag(argv, '--command'),
|
|
125
|
+
reportPath: flag(argv, '--report-path'),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function main(argv: string[]): number {
|
|
130
|
+
const projectPath = flag(argv, '--project-path') ?? '.'
|
|
131
|
+
const w = (s: string) => process.stdout.write(`${s}\n`)
|
|
132
|
+
|
|
133
|
+
if (argv.includes('--list')) {
|
|
134
|
+
for (const s of listSources(projectPath)) {
|
|
135
|
+
w(`${s.adapter}${s.command ? ` (${s.command})` : ''} -> ${s.reportPath}`)
|
|
136
|
+
}
|
|
137
|
+
return 0
|
|
138
|
+
}
|
|
139
|
+
if (argv.includes('--scaffold')) {
|
|
140
|
+
const r = scaffoldSource(projectPath, inputFromArgv(argv))
|
|
141
|
+
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
142
|
+
return r.ok ? 0 : 1
|
|
143
|
+
}
|
|
144
|
+
if (argv.includes('--add')) {
|
|
145
|
+
const r = addSource(projectPath, inputFromArgv(argv))
|
|
146
|
+
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
147
|
+
return r.ok ? 0 : 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
w(
|
|
151
|
+
'usage: manage-scenario-bridge --project-path <dir> --list | --scaffold --adapter <a> [--command <c>] --report-path <p> | --add --adapter <a> [--command <c>] --report-path <p>',
|
|
152
|
+
)
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (import.meta.main) process.exit(main(process.argv.slice(2)))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# manage-spec-anchors
|
|
2
|
+
|
|
3
|
+
The concrete engine for the SDD **spec-anchors** config — the opt-in registry of extra spec anchors
|
|
4
|
+
`discover-specs` scans on top of the three fixed conventions (ADR-0019). A non-user-invocable skill,
|
|
5
|
+
loaded in-session by the `manage` gateway (Housekeeping), carrying a self-contained `.mts` script
|
|
6
|
+
that lists / CRUDs / induces / previews the anchors so users never hand-edit
|
|
7
|
+
`.agents/sdd/spec-anchors.toml`.
|
|
8
|
+
|
|
9
|
+
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
+
- **Script:** [`scripts/manage-spec-anchors.mts`](./scripts/manage-spec-anchors.mts)
|
|
11
|
+
- **Tests:** [`scripts/manage-spec-anchors.test.mts`](./scripts/manage-spec-anchors.test.mts) (`node:test`)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
node scripts/manage-spec-anchors.mts --list
|
|
15
|
+
node scripts/manage-spec-anchors.mts --induce curriculum/web/react/s-01
|
|
16
|
+
node scripts/manage-spec-anchors.mts --preview 'curriculum/*/*/<project>'
|
|
17
|
+
node scripts/manage-spec-anchors.mts --add 'curriculum/*/*/<project>'
|
|
18
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-spec-anchors
|
|
3
|
+
description: "Partial Skill: invoke by name only — corpus/spec-anchors' curation engine for SDD's extra spec-discovery anchors — loaded by the manage gateway (Housekeeping), not triggered by users directly."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
internal: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Manage Spec Anchors
|
|
10
|
+
|
|
11
|
+
The concrete engine for the SDD **spec-anchors** config — the opt-in registry of **extra** spec
|
|
12
|
+
anchors that `discover-specs` scans on top of the three fixed conventions (ADR-0019). It exists so a
|
|
13
|
+
user curates those anchors through a clean interface instead of hand-editing
|
|
14
|
+
`.agents/sdd/spec-anchors.toml`. Loaded **in-session** by the `manage` gateway (Housekeeping group);
|
|
15
|
+
it carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention).
|
|
16
|
+
|
|
17
|
+
## The config it curates
|
|
18
|
+
|
|
19
|
+
`.agents/sdd/spec-anchors.toml` carries one key — `anchors = [ … ]` — a list of repo-relative
|
|
20
|
+
directory patterns. `*` globs one segment; `**` globs **zero or more** segments (any depth,
|
|
21
|
+
including zero); `<project>` globs **and captures** a segment as the spec's name. Each pattern names
|
|
22
|
+
a directory the engine probes for `spec.md`. The three fixed conventions (`.agents/spec/`,
|
|
23
|
+
`.agents/specs/<project>/`, `<project-path>/.agents/spec/`) are **implicit and always scanned** —
|
|
24
|
+
never listed here, and never curatable.
|
|
25
|
+
|
|
26
|
+
## Run an operation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node "<skill>/scripts/manage-spec-anchors.mts" [--root .] <operation>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Operation | Effect |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `--list` | list the three fixed anchors (each explained) + every custom anchor, flagged fixed/custom |
|
|
35
|
+
| `--add <pattern>` | validate + append a custom anchor (creates the config if absent); refuses a fixed convention or a malformed pattern |
|
|
36
|
+
| `--remove <pattern>` | drop a custom anchor; a no-op (config unchanged) when the anchor is absent |
|
|
37
|
+
| `--edit <old> <new>` | replace one custom anchor; refuses a malformed `<new>` |
|
|
38
|
+
| `--induce <path>` | from a sample spec directory, offer a literal-dir candidate and a `<project>` generalization; refuses a path that is not a directory under the repo |
|
|
39
|
+
| `--preview <pattern>` | list the project(s) a candidate pattern would discover, **without** persisting it; a malformed pattern is refused, a zero-match pattern reports none |
|
|
40
|
+
|
|
41
|
+
**Curate with preview.** The intended flow: take the user's sample path → `--induce` it → `--preview`
|
|
42
|
+
each candidate to show the matched projects → confirm with the user → `--add` the chosen pattern.
|
|
43
|
+
Never write a pattern the user has not seen the effect of.
|
|
44
|
+
|
|
45
|
+
## Boundaries
|
|
46
|
+
|
|
47
|
+
Writes **only** `.agents/sdd/spec-anchors.toml` — never a `spec.md`, `status`, `approval`, or a
|
|
48
|
+
freeze; it is operational config, not spec content (so `manage`'s write-ownership guard holds). It
|
|
49
|
+
does **not** scan or list specs — that is `discover-specs`, which *reads* this config. The
|
|
50
|
+
**read-side** fail-safe (an already-corrupted config never breaks discovery) is `discover-specs`'
|
|
51
|
+
guarantee, not this engine's; this engine validates on the **write** side so a bad pattern is never
|
|
52
|
+
persisted.
|
|
53
|
+
|
|
54
|
+
When `node` is absent, an agent performs the same edits by hand: read the `anchors` array, apply the
|
|
55
|
+
CRUD, validate a pattern is repo-relative with only literal / `*` / `**` / `<project>` segments, and never
|
|
56
|
+
write a fixed convention.
|