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,515 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic governance MATCHER for SDD production-chain roles. For a touched
3
+ // file's artifact-type it NAMES, per role, the agent that runs it plus the
4
+ // resolved-actor bar candidates it loads — matching governances by
5
+ // metadata{artifact-type, actor, gate} across the caller-passed project anchors,
6
+ // the matched plugin squad, and the sdd defaults. It does NOT order by precedence
7
+ // or compose: each bar's candidates come back BUCKETED BY TIER (project /
8
+ // project-root / plugin / sdd), and the consuming agent loads each ref and
9
+ // composes by precedence (sdd-default < plugin < project-root < project), reading
10
+ // each governance's own `compose` at load time (see sdd:plugin-contract-governance).
11
+ // The conductor and the cold judges never hand-enumerate — they run this and load
12
+ // what it names.
13
+ //
14
+ // Anchors are CALLER-PASSED, never discovered: --project <path> (the file's own
15
+ // project) and optional --project-root <path> (the outer shared layer in a
16
+ // monorepo); a single-project repo passes only --project. --root is the registry
17
+ // location (`.agents/universal-plugin.json`), default ".".
18
+ //
19
+ // Pure functions are exported for node:test; running the file directly drives the
20
+ // CLI. No dependencies — plain node strips the types.
21
+
22
+ import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+
25
+ // ─── the closed sets ───────────────────────────────────────────────────────────
26
+
27
+ export const ROLE_KEYS = ['spec-producer', 'solution-producer', 'spec-judge', 'impl-producer', 'impl-judge'] as const
28
+ export type RoleKey = (typeof ROLE_KEYS)[number]
29
+
30
+ export const BAR_KEYS = ['oracle-spec', 'builder-spec', 'builder-impl', 'architect-spec', 'architect-impl'] as const
31
+ export type BarKey = (typeof BAR_KEYS)[number]
32
+
33
+ export const ACTORS = ['oracle', 'builder', 'architect'] as const
34
+ export const GATES = ['spec', 'impl'] as const
35
+
36
+ // The resolved-actor bars each role loads (plugin-contract-governance "Which
37
+ // governances each role loads"). Only the resolved-actor bars are matched here;
38
+ // the FIXED-UNIVERSAL governances (ownership, lifecycle, spec-format,
39
+ // suite-format, gate-validation, combat-log) are invariant per role and stay
40
+ // declared in each role/agent definition — this matcher does not re-emit them.
41
+ export const ROLE_LOADOUT: Record<RoleKey, { bars: BarKey[] }> = {
42
+ 'spec-producer': { bars: ['oracle-spec', 'builder-spec'] },
43
+ 'solution-producer': { bars: ['architect-spec'] },
44
+ 'spec-judge': { bars: ['oracle-spec', 'builder-spec', 'architect-spec'] },
45
+ 'impl-producer': { bars: ['builder-impl', 'architect-impl'] },
46
+ 'impl-judge': { bars: ['builder-impl', 'architect-impl'] },
47
+ }
48
+
49
+ // The SDD-default agent per role. A null ref means the conductor runs the role
50
+ // INLINE in the main session (spec/solution-producer) or via a generic spawned
51
+ // builder (impl-producer); both are recorded produced-by sdd:automaton. The two
52
+ // judges are spawned cold by name.
53
+ export const SDD_DEFAULT_AGENT: Record<RoleKey, string | null> = {
54
+ 'spec-producer': null,
55
+ 'solution-producer': null,
56
+ 'spec-judge': 'sdd-spec-judge',
57
+ 'impl-producer': null,
58
+ 'impl-judge': 'sdd-impl-judge',
59
+ }
60
+
61
+ // ─── types ──────────────────────────────────────────────────────────────────────
62
+
63
+ // A squad serves a SET of artifact-types with one production chain. A plugin lists
64
+ // one or more squads (specialists-and-squads.md "Registry SHAPE"); a type appears
65
+ // in at most one squad per plugin.
66
+ export interface Squad {
67
+ 'artifact-types': string[]
68
+ roles: Partial<Record<RoleKey, string | null>>
69
+ governances: Partial<Record<BarKey, string | null>>
70
+ }
71
+
72
+ export interface RegistryEntry {
73
+ name: string
74
+ version?: string
75
+ squads: Squad[]
76
+ }
77
+
78
+ export interface Registry {
79
+ 'sdd-plugins': RegistryEntry[]
80
+ }
81
+
82
+ // A matched squad carries its owning plugin name (for `<plugin>:<bar>` /
83
+ // `<plugin>-<role>` refs).
84
+ export interface SquadMatch {
85
+ plugin: string
86
+ squad: Squad
87
+ }
88
+
89
+ export interface GovMetadata {
90
+ artifactType: string | null
91
+ actor: string | null
92
+ gate: string | null
93
+ compose: 'union' | 'replace' // parsed but NOT emitted — the agent reads it from the loaded file
94
+ }
95
+
96
+ // The two project tiers a candidate can come from. project = the file's own
97
+ // project (most specific); project-root = the outer shared layer in a monorepo.
98
+ export type Tier = 'project' | 'project-root'
99
+
100
+ export interface GovCandidate extends GovMetadata {
101
+ tier: Tier
102
+ path: string // root-relative file path (direct-read ref)
103
+ }
104
+
105
+ // One bar's matched candidates, BUCKETED BY TIER — never ordered, never composed.
106
+ // project / project-root are direct-read file paths; plugin / sdd are harness-load
107
+ // skill refs. The agent applies precedence sdd < plugin < project-root < project.
108
+ export interface BarPlan {
109
+ key: BarKey
110
+ candidates: {
111
+ project: string[]
112
+ 'project-root': string[]
113
+ plugin: string | null // <plugin>:<bar> harness-load ref, or null when no squad bar
114
+ sdd: string // sdd:<key>-governance harness-load ref (always present)
115
+ }
116
+ }
117
+
118
+ export interface AgentResolution {
119
+ source: 'plugin' | 'sdd'
120
+ ref: string | null // agent name; null = inline (producer) / generic builder (impl-producer)
121
+ }
122
+
123
+ export interface RolePlan {
124
+ role: RoleKey
125
+ agent: AgentResolution
126
+ bars: BarPlan[] // resolved-actor bars only; fixed-universal live in the role/agent def
127
+ }
128
+
129
+ export interface LoadPlan {
130
+ artifactType: string | null
131
+ status: 'complete' | 'needs-input'
132
+ plugin: string | null
133
+ ambiguous: string[] // plugin names, when status is needs-input
134
+ roles: RolePlan[]
135
+ }
136
+
137
+ // ─── registry parse + migrate-on-read ───────────────────────────────────────────
138
+
139
+ // Within a squad: rename the legacy plan-producer role key to solution-producer
140
+ // and expand the flat governances{oracle,builder,architect} to the Model-B
141
+ // (actor,gate) keys.
142
+ function migrateRoles(rolesIn: Record<string, string | null> | undefined): Partial<Record<RoleKey, string | null>> {
143
+ const roles = { ...(rolesIn ?? {}) }
144
+ if ('plan-producer' in roles) {
145
+ const legacy = roles['plan-producer']
146
+ if (!('solution-producer' in roles)) roles['solution-producer'] = legacy
147
+ delete roles['plan-producer']
148
+ }
149
+ return roles as Partial<Record<RoleKey, string | null>>
150
+ }
151
+
152
+ function migrateGovernances(gIn: Record<string, string | null> | undefined): Partial<Record<BarKey, string | null>> {
153
+ const g = (gIn ?? {}) as Record<string, string | null>
154
+ const isLegacy = 'oracle' in g || 'builder' in g || 'architect' in g
155
+ return isLegacy
156
+ ? {
157
+ 'oracle-spec': g.oracle ?? null,
158
+ 'builder-spec': g.builder ?? null,
159
+ 'builder-impl': g.builder ?? null,
160
+ 'architect-spec': g.architect ?? null,
161
+ 'architect-impl': g.architect ?? null,
162
+ }
163
+ : { ...(g as Partial<Record<BarKey, string | null>>) }
164
+ }
165
+
166
+ function migrateSquad(sq: Record<string, unknown>): Squad {
167
+ const types = (sq['artifact-types'] ?? sq.domains ?? []) as string[]
168
+ return {
169
+ 'artifact-types': Array.isArray(types) ? types : [],
170
+ roles: migrateRoles(sq.roles as Record<string, string | null>),
171
+ governances: migrateGovernances(sq.governances as Record<string, string | null>),
172
+ }
173
+ }
174
+
175
+ // Migrate a registry entry on read. The legacy shape carried `domains[]` + a
176
+ // shared `roles{}` + `governances{}` directly on the entry; fold it into one
177
+ // squad. A live registry still on the legacy shape is migrated on encounter
178
+ // (plugin-contract-governance).
179
+ export function migrateEntry(entry: Record<string, unknown>): RegistryEntry {
180
+ const squads = Array.isArray(entry.squads)
181
+ ? (entry.squads as Record<string, unknown>[]).map(migrateSquad)
182
+ : [migrateSquad({ 'artifact-types': entry.domains, roles: entry.roles, governances: entry.governances })]
183
+ return { name: entry.name as string, version: entry.version as string | undefined, squads }
184
+ }
185
+
186
+ export function parseRegistry(text: string): Registry {
187
+ let raw: unknown
188
+ try {
189
+ raw = JSON.parse(text)
190
+ } catch (e) {
191
+ throw new Error(`registry is not valid JSON: ${(e as Error).message}`)
192
+ }
193
+ if (!raw || typeof raw !== 'object' || !Array.isArray((raw as { 'sdd-plugins': unknown })['sdd-plugins']))
194
+ throw new Error('registry has no sdd-plugins array')
195
+ const entries = (raw as { 'sdd-plugins': Record<string, unknown>[] })['sdd-plugins'].map(migrateEntry)
196
+ return { 'sdd-plugins': entries }
197
+ }
198
+
199
+ // A project anchor with no registry resolves to all-SDD-defaults — a missing file
200
+ // is legal, not an error.
201
+ export function loadRegistry(root: string): Registry {
202
+ const path = join(root, '.agents', 'universal-plugin.json')
203
+ if (!existsSync(path)) return { 'sdd-plugins': [] }
204
+ return parseRegistry(readFileSync(path, 'utf8'))
205
+ }
206
+
207
+ // ─── artifact-type tiebreaker map (.agents/sdd/artifact-types.toml) ──────────────
208
+
209
+ // An OPTIONAL, agent-maintained lookup that records resolved path->type bindings
210
+ // for the ambiguities convention can't settle (the artifact-type model). It is
211
+ // NOT the primary classifier — convention is, and that is the conductor's
212
+ // judgment; this table is consulted only on a known ambiguity or a user-flagged
213
+ // path. A flat TOML map "<path-or-glob>" = "<artifact-type>"; most-specific glob
214
+ // wins. No TOML dependency — the table is intentionally flat key=value.
215
+ export interface TypeBinding {
216
+ glob: string
217
+ type: string
218
+ }
219
+
220
+ export function parseArtifactTypeMap(text: string): TypeBinding[] {
221
+ const out: TypeBinding[] = []
222
+ for (const raw of text.split('\n')) {
223
+ const line = raw.trim()
224
+ if (!line || line.startsWith('#') || line.startsWith('[')) continue // blanks, comments, [section] headers
225
+ const m = /^(?:"([^"]+)"|'([^']+)'|(\S+))\s*=\s*(?:"([^"]+)"|'([^']+)')\s*(?:#.*)?$/.exec(line)
226
+ if (!m) continue
227
+ const glob = m[1] ?? m[2] ?? m[3]
228
+ const type = m[4] ?? m[5]
229
+ if (glob && type) out.push({ glob, type })
230
+ }
231
+ return out
232
+ }
233
+
234
+ // Compile a glob to a full-match RegExp: ** spans path separators, * does not.
235
+ function globToRegExp(glob: string): RegExp {
236
+ const re = glob
237
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex specials (leave * ?)
238
+ .replace(/\*+/g, (m) => (m.length > 1 ? '.*' : '[^/]*')) // ** spans separators, * does not
239
+ .replace(/\?/g, '.')
240
+ return new RegExp(`^${re}$`)
241
+ }
242
+
243
+ // Resolve a file path to an artifact-type via the tiebreaker map: the
244
+ // most-specific matching glob wins (most literal characters, then longest
245
+ // pattern). Returns null when nothing matches — the caller falls back to
246
+ // convention.
247
+ export function resolveArtifactTypeFromMap(map: TypeBinding[], path: string): string | null {
248
+ const matches = map.filter((b) => globToRegExp(b.glob).test(path))
249
+ if (matches.length === 0) return null
250
+ const literal = (g: string) => g.replace(/[*?]/g, '').length
251
+ matches.sort((a, b) => literal(b.glob) - literal(a.glob) || b.glob.length - a.glob.length)
252
+ return matches[0].type
253
+ }
254
+
255
+ // Load the optional tiebreaker map. A missing file is legal (most projects need
256
+ // none) — returns an empty table.
257
+ export function loadArtifactTypeMap(root: string): TypeBinding[] {
258
+ const path = join(root, '.agents', 'sdd', 'artifact-types.toml')
259
+ if (!existsSync(path)) return []
260
+ return parseArtifactTypeMap(readFileSync(path, 'utf8'))
261
+ }
262
+
263
+ // ─── project governance discovery ───────────────────────────────────────────────
264
+
265
+ function frontmatter(text: string): string[] {
266
+ const m = /^---\n([\s\S]*?)\n---/.exec(text)
267
+ return m ? m[1].split('\n') : []
268
+ }
269
+
270
+ const unquote = (s: string) => s.trim().replace(/^["']|["']$/g, '')
271
+
272
+ // Read metadata{artifact-type?, actor, gate, compose} from a governance file's
273
+ // frontmatter. Returns null when there is no metadata block with an actor+gate —
274
+ // a plain doc that is not a resolvable bar.
275
+ export function parseGovernanceFrontmatter(text: string): GovMetadata | null {
276
+ const lines = frontmatter(text)
277
+ let inMeta = false
278
+ let artifactType: string | null = null
279
+ let actor: string | null = null
280
+ let gate: string | null = null
281
+ let compose: 'union' | 'replace' = 'union'
282
+
283
+ for (const line of lines) {
284
+ if (/^metadata:\s*$/.test(line)) {
285
+ inMeta = true
286
+ continue
287
+ }
288
+ if (inMeta && /^\S/.test(line)) inMeta = false // dedent ends the block
289
+ if (!inMeta) continue
290
+ const at = /^\s+artifact-type:\s*(.+)$/.exec(line)
291
+ if (at) artifactType = unquote(at[1])
292
+ const ac = /^\s+actor:\s*(.+)$/.exec(line)
293
+ if (ac) actor = unquote(ac[1])
294
+ const ga = /^\s+gate:\s*(.+)$/.exec(line)
295
+ if (ga) gate = unquote(ga[1])
296
+ const co = /^\s+compose:\s*(.+)$/.exec(line)
297
+ if (co) compose = unquote(co[1]) === 'replace' ? 'replace' : 'union'
298
+ }
299
+
300
+ if (!actor || !gate) return null
301
+ return { artifactType, actor, gate, compose }
302
+ }
303
+
304
+ // Collect project governance candidates from the CALLER-PASSED anchors — never a
305
+ // tree walk. Each anchor names a tier and a root; the candidate carries its tier
306
+ // (for the agent's precedence) and its root-relative path (the direct-read ref).
307
+ // A missing `.agents/governances/` at an anchor contributes nothing. Both .md and
308
+ // SKILL.md are read in place — no build.
309
+ export function collectAnchorGovernances(anchors: { tier: Tier; root: string }[]): GovCandidate[] {
310
+ const out: GovCandidate[] = []
311
+ for (const { tier, root } of anchors) {
312
+ const dir = join(root, '.agents', 'governances')
313
+ let entries: Dirent[]
314
+ try {
315
+ entries = readdirSync(dir, { withFileTypes: true })
316
+ } catch {
317
+ continue
318
+ }
319
+ for (const e of entries) {
320
+ if (!e.isFile() || !e.name.endsWith('.md')) continue
321
+ const meta = parseGovernanceFrontmatter(readFileSync(join(dir, e.name), 'utf8'))
322
+ if (meta) out.push({ ...meta, tier, path: join(root, '.agents', 'governances', e.name) })
323
+ }
324
+ }
325
+ return out
326
+ }
327
+
328
+ // ─── plugin matching ────────────────────────────────────────────────────────────
329
+
330
+ // Match an artifact-type against each plugin's squads: the squad whose
331
+ // artifact-types contains it serves the file. Zero matches → all SDD defaults;
332
+ // one → that squad; two or more distinct plugins → ambiguous (the conductor
333
+ // consults the contested-type choice or asks).
334
+ export function matchSquad(
335
+ registry: Registry,
336
+ artifactType: string | null,
337
+ ): { match: SquadMatch | null; ambiguous: string[] } {
338
+ if (!artifactType) return { match: null, ambiguous: [] }
339
+ const matches: SquadMatch[] = []
340
+ for (const p of registry['sdd-plugins'])
341
+ for (const sq of p.squads ?? [])
342
+ if (sq['artifact-types']?.includes(artifactType)) matches.push({ plugin: p.name, squad: sq })
343
+ if (matches.length === 0) return { match: null, ambiguous: [] }
344
+ if (matches.length === 1) return { match: matches[0], ambiguous: [] }
345
+ return { match: null, ambiguous: [...new Set(matches.map((m) => m.plugin))] }
346
+ }
347
+
348
+ // ─── bar matching ───────────────────────────────────────────────────────────────
349
+
350
+ // Match one (actor, gate) bar across the sources and return the candidates
351
+ // BUCKETED BY TIER — no ordering, no compose collapse. A project candidate matches
352
+ // when its frontmatter (actor, gate) matches and its artifact-type is the file's
353
+ // type OR typeless (a typeless project bar applies to every type). The agent reads
354
+ // each candidate's own `compose` at load time and composes by precedence
355
+ // (sdd-default < plugin < project-root < project).
356
+ export function matchBar(
357
+ artifactType: string | null,
358
+ actor: string,
359
+ gate: string,
360
+ ctx: { match: SquadMatch | null; projectGovs: GovCandidate[] },
361
+ ): BarPlan {
362
+ const key = `${actor}-${gate}` as BarKey
363
+ const bucket = (tier: Tier) =>
364
+ ctx.projectGovs
365
+ .filter((c) => c.tier === tier && c.actor === actor && c.gate === gate)
366
+ .filter((c) => c.artifactType === artifactType || c.artifactType === null)
367
+ .map((c) => c.path)
368
+
369
+ const pluginBar = ctx.match?.squad.governances?.[key]
370
+ return {
371
+ key,
372
+ candidates: {
373
+ project: bucket('project'),
374
+ 'project-root': bucket('project-root'),
375
+ plugin: ctx.match && pluginBar ? `${ctx.match.plugin}:${pluginBar}` : null,
376
+ sdd: `sdd:${key}-governance`,
377
+ },
378
+ }
379
+ }
380
+
381
+ // ─── role + plan resolution ─────────────────────────────────────────────────────
382
+
383
+ // Resolve the agent that runs a role: a named plugin delegate, the SDD default,
384
+ // or — for a present-but-missing role key — the <plugin>-<role> convention.
385
+ export function resolveAgent(role: RoleKey, match: SquadMatch | null): AgentResolution {
386
+ if (!match) return { source: 'sdd', ref: SDD_DEFAULT_AGENT[role] }
387
+ if (role in match.squad.roles) {
388
+ const named = match.squad.roles[role]
389
+ if (named) return { source: 'plugin', ref: named }
390
+ return { source: 'sdd', ref: SDD_DEFAULT_AGENT[role] } // explicit null = SDD default
391
+ }
392
+ return { source: 'plugin', ref: `${match.plugin}-${role}` } // omitted = convention
393
+ }
394
+
395
+ export function resolveRole(
396
+ role: RoleKey,
397
+ artifactType: string | null,
398
+ ctx: { match: SquadMatch | null; projectGovs: GovCandidate[] },
399
+ ): RolePlan {
400
+ const bars = ROLE_LOADOUT[role].bars.map((b) => {
401
+ const [actor, gate] = b.split('-')
402
+ return matchBar(artifactType, actor, gate, ctx)
403
+ })
404
+ return { role, agent: resolveAgent(role, ctx.match), bars }
405
+ }
406
+
407
+ export function buildLoadPlan(artifactType: string | null, registry: Registry, projectGovs: GovCandidate[]): LoadPlan {
408
+ const { match, ambiguous } = matchSquad(registry, artifactType)
409
+ if (ambiguous.length > 0)
410
+ return {
411
+ artifactType,
412
+ status: 'needs-input',
413
+ plugin: null,
414
+ ambiguous,
415
+ roles: [],
416
+ }
417
+ const ctx = { match, projectGovs }
418
+ return {
419
+ artifactType,
420
+ status: 'complete',
421
+ plugin: match?.plugin ?? null,
422
+ ambiguous: [],
423
+ roles: ROLE_KEYS.map((r) => resolveRole(r, artifactType, ctx)),
424
+ }
425
+ }
426
+
427
+ // ─── registry structural validation (the no-artifact-type CLI gate) ─────────────
428
+
429
+ // Validate the registry is well-formed and unambiguous: known role/governance
430
+ // keys, a type in at most one squad per plugin, and no artifact-type claimed by
431
+ // two plugins (which would force every spec of that type into needs-input).
432
+ export function validateRegistry(registry: Registry): string[] {
433
+ const v: string[] = []
434
+ const typeOwners = new Map<string, string[]>()
435
+ for (const entry of registry['sdd-plugins']) {
436
+ const where = entry.name || '<unnamed plugin>'
437
+ if (!entry.name) v.push('an sdd-plugins entry has no name')
438
+ if (!Array.isArray(entry.squads)) {
439
+ v.push(`${where}: squads is not an array`)
440
+ continue
441
+ }
442
+ const seenTypes = new Set<string>()
443
+ for (const sq of entry.squads) {
444
+ if (!Array.isArray(sq['artifact-types'])) v.push(`${where}: a squad's artifact-types is not an array`)
445
+ for (const r of Object.keys(sq.roles ?? {}))
446
+ if (!ROLE_KEYS.includes(r as RoleKey)) v.push(`${where}: unknown role key "${r}"`)
447
+ for (const k of Object.keys(sq.governances ?? {}))
448
+ if (!BAR_KEYS.includes(k as BarKey)) v.push(`${where}: unknown governance key "${k}"`)
449
+ for (const t of sq['artifact-types'] ?? []) {
450
+ if (seenTypes.has(t)) v.push(`${where}: artifact-type "${t}" appears in more than one squad`)
451
+ seenTypes.add(t)
452
+ }
453
+ }
454
+ for (const t of seenTypes) typeOwners.set(t, [...(typeOwners.get(t) ?? []), where])
455
+ }
456
+ for (const [t, owners] of typeOwners)
457
+ if (owners.length > 1)
458
+ v.push(`artifact-type "${t}" is claimed by ${owners.length} plugins (${owners.join(', ')}) — ambiguous`)
459
+ return v
460
+ }
461
+
462
+ // ─── CLI entry ──────────────────────────────────────────────────────────────────
463
+
464
+ export function main(argv: string[]): number {
465
+ const root = argv.includes('--root') ? argv[argv.indexOf('--root') + 1] : '.'
466
+ const explicitType = argv.includes('--artifact-type') ? argv[argv.indexOf('--artifact-type') + 1] : null
467
+ const pathArg = argv.includes('--path') ? argv[argv.indexOf('--path') + 1] : null
468
+ // Caller-passed project anchors. --project defaults to --root (single-project);
469
+ // --project-root is the outer shared layer in a monorepo (omitted otherwise).
470
+ const projectArg = argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : root
471
+ const projectRootArg = argv.includes('--project-root') ? argv[argv.indexOf('--project-root') + 1] : null
472
+ const anchors: { tier: Tier; root: string }[] = [{ tier: 'project', root: projectArg }]
473
+ if (projectRootArg) anchors.push({ tier: 'project-root', root: projectRootArg })
474
+
475
+ // --artifact-type wins (explicit override); else --path consults the optional
476
+ // tiebreaker map (most-specific glob). A --path with no map match stays null —
477
+ // the caller falls back to convention, it is not a registry-validation request.
478
+ let artifactType = explicitType
479
+ if (!artifactType && pathArg) artifactType = resolveArtifactTypeFromMap(loadArtifactTypeMap(root), pathArg)
480
+
481
+ let registry: Registry
482
+ try {
483
+ registry = loadRegistry(root)
484
+ } catch (e) {
485
+ console.error(`✗ ${(e as Error).message}`)
486
+ return 1
487
+ }
488
+
489
+ if (!artifactType && pathArg) {
490
+ process.stdout.write(
491
+ `${JSON.stringify({ path: pathArg, artifactType: null, note: 'no tiebreaker match — classify by convention' }, null, 2)}\n`,
492
+ )
493
+ return 0
494
+ }
495
+
496
+ // No artifact-type and no path → validate the registry is well-formed + unambiguous.
497
+ if (!artifactType) {
498
+ const violations = validateRegistry(registry)
499
+ if (violations.length) {
500
+ for (const line of violations) console.error(`✗ ${line}`)
501
+ return 1
502
+ }
503
+ process.stdout.write('governance registry OK\n')
504
+ return 0
505
+ }
506
+
507
+ // With an artifact-type → emit the per-role plan (agent + tier-bucketed bars).
508
+ const plan = buildLoadPlan(artifactType, registry, collectAnchorGovernances(anchors))
509
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`)
510
+ if (plan.status === 'needs-input')
511
+ console.error(`needs-input: artifact-type "${artifactType}" is claimed by ${plan.ambiguous.join(', ')}`)
512
+ return 0
513
+ }
514
+
515
+ if (import.meta.main) process.exit(main(process.argv.slice(2)))
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: resolve-tracking
3
+ description: "Partial Skill: invoke by name only — intake/resolve-tracking's engine that resolves an artifact's tracked-or-ignored signal — run by the conductor at intake, not triggered by users directly."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # Resolve Tracking
10
+
11
+ The concrete engine for **tracking resolution** — the second escape-hatch trigger
12
+ (`.agents/specs/sdd/intake/README.md`). For one touched artifact it decides **tracked** (SDD
13
+ governs it — spec + gates) or **ignored** (SDD does not govern it; it still gets built) and
14
+ reports which step decided it, so the conductor can skip a task outright (no CR, no draft, no
15
+ gate, no record) when it resolves ignored. The split mirrors git's **tracked vs ignored** files.
16
+ It carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention).
17
+
18
+ ## Run the resolution
19
+
20
+ ```bash
21
+ node "<skill>/scripts/resolve-tracking.mts" --root . --path <repo-relative-path> [--artifact-type <type>] [--explicit tracked|ignored]
22
+ ```
23
+
24
+ - `--root` is the repo root (default `.`) — where `.agents/sdd/.sddignore` is read from.
25
+ - `--path <path>` is the artifact's repo-relative path. Required unless validating the file.
26
+ - `--artifact-type <type>` enables the kind-default step for `skill` / `subagent` / `command`
27
+ (a fixed location convention). Omit it (or pass `agents-section` or any code artifact-type) to
28
+ skip straight from `.sddignore` to fail-closed — there is no kind default for those.
29
+ - `--explicit tracked|ignored` short-circuits resolution — nothing else is consulted.
30
+ - No `--path` → **validates** `.agents/sdd/.sddignore` is well-formed (`.sddignore OK`, or
31
+ per-line parse notes); a missing file is legal and also reports OK.
32
+
33
+ Output is two lines: the verdict (`tracked` | `ignored`) and a `reason:` line naming the
34
+ deciding step, so the caller can log or surface why.
35
+
36
+ ## Resolution order
37
+
38
+ 1. **Explicit override** (`--explicit`) — wins outright.
39
+ 2. **`.agents/sdd/.sddignore`** — optional, curated **gitignore-syntax** file. Blank lines and
40
+ `#` comments are skipped. A leading `!` marks the path **tracked** (re-include); any other
41
+ pattern marks it **ignored**. Matching is **last-match-wins**: the last rule whose pattern
42
+ matches the path decides, so a later `!` line re-tracks a path an earlier pattern ignored.
43
+ Supports `**` (spans path separators), `*` (within a segment), a leading `/` (anchor to root),
44
+ and a trailing `/` (match a directory and its contents). A path no rule matches **falls
45
+ through**. Universal — applies to any artifact-type.
46
+ 3. **Kind default** — `skill` / `subagent` / `command` only: project-private paths
47
+ (`.agents/skills|agents|commands/**`) are **ignored**; project-public / shipped paths
48
+ (`skills/**`, `plugins/*/skills/**`, `packages/*/skills/**`, and the `agents`/`commands`
49
+ equivalents) are **tracked**. No default for `agents-section` or any code artifact-type.
50
+ 4. **Fail closed** — no signal resolves **tracked**. Govern by default; a false ignore (dropping
51
+ a real record) is the one failure mode this hatch must never produce.
52
+
53
+ ## Notes
54
+
55
+ - This is a **pure derivation** over fixed conventions plus the optional `.sddignore` — it
56
+ consults no per-project registry of paths and writes nothing. `.sddignore` is the only
57
+ per-project state: optional, mutable, edited in place like a `.gitignore` (curated by
58
+ `manage-ignore`), never an append-only log.
59
+ - One deliberate simplification vs git: matching is **pattern-level and last-match-wins**; the
60
+ resolver does **not** replicate git's directory-pruning quirk (a `!` under an excluded parent
61
+ still re-tracks).
62
+ - The kind-default location globs are fixed in this script, not configurable — a project that
63
+ disagrees with a specific binding uses `.sddignore` to override it (keep the low-friction
64
+ default, add the override valve).