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.
Files changed (133) hide show
  1. package/.claude-plugin/plugin.json +17 -0
  2. package/.codex-plugin/plugin.json +17 -0
  3. package/.plugin/plugin.json +17 -0
  4. package/README.md +159 -0
  5. package/agents/sdd-automaton.md +97 -0
  6. package/agents/sdd-impl-judge.md +214 -0
  7. package/agents/sdd-scanner.md +120 -0
  8. package/agents/sdd-spec-judge.md +224 -0
  9. package/agents/sdd-warden.md +101 -0
  10. package/package.json +24 -0
  11. package/skills/align-spec/README.md +20 -0
  12. package/skills/align-spec/SKILL.md +111 -0
  13. package/skills/align-spec/scripts/align-spec.mts +187 -0
  14. package/skills/architect-impl-governance/README.md +46 -0
  15. package/skills/architect-impl-governance/SKILL.md +45 -0
  16. package/skills/architect-spec-governance/README.md +48 -0
  17. package/skills/architect-spec-governance/SKILL.md +59 -0
  18. package/skills/blast-estimate/README.md +47 -0
  19. package/skills/blast-estimate/SKILL.md +133 -0
  20. package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
  21. package/skills/builder-impl-governance/README.md +47 -0
  22. package/skills/builder-impl-governance/SKILL.md +47 -0
  23. package/skills/builder-spec-governance/README.md +49 -0
  24. package/skills/builder-spec-governance/SKILL.md +36 -0
  25. package/skills/check-partition-quality/README.md +22 -0
  26. package/skills/check-partition-quality/SKILL.md +51 -0
  27. package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
  28. package/skills/check-plan-safety/README.md +17 -0
  29. package/skills/check-plan-safety/SKILL.md +60 -0
  30. package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
  31. package/skills/check-project-specs/README.md +19 -0
  32. package/skills/check-project-specs/SKILL.md +69 -0
  33. package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
  34. package/skills/check-scenario-overlap/README.md +19 -0
  35. package/skills/check-scenario-overlap/SKILL.md +74 -0
  36. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
  37. package/skills/check-spec-structure/README.md +17 -0
  38. package/skills/check-spec-structure/SKILL.md +66 -0
  39. package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
  40. package/skills/collision-ladder/README.md +18 -0
  41. package/skills/collision-ladder/SKILL.md +83 -0
  42. package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
  43. package/skills/combat-log-governance/README.md +13 -0
  44. package/skills/combat-log-governance/SKILL.md +257 -0
  45. package/skills/concept-index/README.md +13 -0
  46. package/skills/concept-index/SKILL.md +38 -0
  47. package/skills/concept-index/scripts/concept-index.mts +245 -0
  48. package/skills/discover-plans/README.md +16 -0
  49. package/skills/discover-plans/SKILL.md +74 -0
  50. package/skills/discover-plans/scripts/discover-plans.mts +212 -0
  51. package/skills/discover-specs/README.md +15 -0
  52. package/skills/discover-specs/SKILL.md +76 -0
  53. package/skills/discover-specs/scripts/discover-specs.mts +396 -0
  54. package/skills/doctrine-loop/README.md +15 -0
  55. package/skills/doctrine-loop/SKILL.md +97 -0
  56. package/skills/formation-loop/README.md +17 -0
  57. package/skills/formation-loop/SKILL.md +140 -0
  58. package/skills/gate-validation-governance/README.md +12 -0
  59. package/skills/gate-validation-governance/SKILL.md +87 -0
  60. package/skills/impl-producer-governance/README.md +48 -0
  61. package/skills/impl-producer-governance/SKILL.md +85 -0
  62. package/skills/init/README.md +27 -0
  63. package/skills/init/SKILL.md +68 -0
  64. package/skills/init/scripts/wire-statusline.mts +276 -0
  65. package/skills/lifecycle-governance/README.md +11 -0
  66. package/skills/lifecycle-governance/SKILL.md +168 -0
  67. package/skills/manage/README.md +9 -0
  68. package/skills/manage/SKILL.md +62 -0
  69. package/skills/manage-ignore/README.md +19 -0
  70. package/skills/manage-ignore/SKILL.md +52 -0
  71. package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
  72. package/skills/manage-scenario-bridge/README.md +20 -0
  73. package/skills/manage-scenario-bridge/SKILL.md +60 -0
  74. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
  75. package/skills/manage-spec-anchors/README.md +18 -0
  76. package/skills/manage-spec-anchors/SKILL.md +56 -0
  77. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
  78. package/skills/mission-graph/README.md +15 -0
  79. package/skills/mission-graph/SKILL.md +67 -0
  80. package/skills/mission-graph/scripts/mission-graph.mts +844 -0
  81. package/skills/oracle-spec-governance/README.md +45 -0
  82. package/skills/oracle-spec-governance/SKILL.md +45 -0
  83. package/skills/ownership-governance/README.md +65 -0
  84. package/skills/ownership-governance/SKILL.md +104 -0
  85. package/skills/pause-mission/README.md +18 -0
  86. package/skills/pause-mission/SKILL.md +112 -0
  87. package/skills/place-node/README.md +12 -0
  88. package/skills/place-node/SKILL.md +47 -0
  89. package/skills/place-node/scripts/place-node.mts +157 -0
  90. package/skills/plan-retirement/README.md +32 -0
  91. package/skills/plan-retirement/SKILL.md +90 -0
  92. package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
  93. package/skills/plugin-contract-governance/README.md +12 -0
  94. package/skills/plugin-contract-governance/SKILL.md +112 -0
  95. package/skills/remediation-governance/README.md +46 -0
  96. package/skills/remediation-governance/SKILL.md +78 -0
  97. package/skills/resolve-governances/README.md +18 -0
  98. package/skills/resolve-governances/SKILL.md +50 -0
  99. package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
  100. package/skills/resolve-tracking/SKILL.md +64 -0
  101. package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
  102. package/skills/resume-mission/README.md +12 -0
  103. package/skills/resume-mission/SKILL.md +53 -0
  104. package/skills/scaffold-project-spec/README.md +7 -0
  105. package/skills/scaffold-project-spec/SKILL.md +192 -0
  106. package/skills/sdd/README.md +7 -0
  107. package/skills/sdd/SKILL.md +92 -0
  108. package/skills/solution-producer-governance/README.md +9 -0
  109. package/skills/solution-producer-governance/SKILL.md +44 -0
  110. package/skills/spec-format-governance/README.md +73 -0
  111. package/skills/spec-format-governance/SKILL.md +114 -0
  112. package/skills/spec-gate/README.md +26 -0
  113. package/skills/spec-gate/SKILL.md +201 -0
  114. package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
  115. package/skills/spec-gate/scripts/check-suite.mts +501 -0
  116. package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
  117. package/skills/spec-producer-governance/README.md +7 -0
  118. package/skills/spec-producer-governance/SKILL.md +86 -0
  119. package/skills/spec-structure-governance/README.md +40 -0
  120. package/skills/spec-structure-governance/SKILL.md +169 -0
  121. package/skills/ssa-lowering/README.md +26 -0
  122. package/skills/ssa-lowering/SKILL.md +181 -0
  123. package/skills/start-mission/README.md +7 -0
  124. package/skills/start-mission/SKILL.md +115 -0
  125. package/skills/suite-format-governance/README.md +75 -0
  126. package/skills/suite-format-governance/SKILL.md +299 -0
  127. package/skills/suite-format-governance/references/rubric.md +313 -0
  128. package/skills/touch-set-correction/README.md +16 -0
  129. package/skills/touch-set-correction/SKILL.md +67 -0
  130. package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
  131. package/skills/verify-scenarios/README.md +17 -0
  132. package/skills/verify-scenarios/SKILL.md +109 -0
  133. 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.