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,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.