cyber-sdd 0.7.0 → 0.7.2

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 (150) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/CHANGELOG.md +320 -0
  4. package/agents/sdd-scanner.md +1 -1
  5. package/package.json +11 -12
  6. package/plugin.json +55 -0
  7. package/skills/align-spec/SKILL.md +1 -1
  8. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  9. package/skills/blast-estimate/README.md +3 -3
  10. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  11. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  12. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  13. package/skills/check-plan-safety/SKILL.md +2 -3
  14. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  15. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  16. package/skills/check-project-specs/SKILL.md +1 -1
  17. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  18. package/skills/check-retired-terms/SKILL.md +2 -3
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  20. package/skills/check-scenario-overlap/SKILL.md +1 -2
  21. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  22. package/skills/check-spec-references/SKILL.md +2 -2
  23. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  24. package/skills/check-spec-structure/SKILL.md +1 -2
  25. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  26. package/skills/collision-ladder/README.md +2 -2
  27. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  28. package/skills/combat-log-governance/SKILL.md +1 -1
  29. package/skills/concept-index/SKILL.md +1 -1
  30. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  31. package/skills/discover-plans/README.md +3 -3
  32. package/skills/discover-plans/SKILL.md +1 -1
  33. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  34. package/skills/discover-specs/README.md +3 -3
  35. package/skills/discover-specs/SKILL.md +1 -1
  36. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  37. package/skills/doctrine-loop/SKILL.md +1 -1
  38. package/skills/gate-validation-governance/SKILL.md +1 -1
  39. package/skills/init/README.md +2 -2
  40. package/skills/init/scripts/wire-statusline.mjs +168 -12
  41. package/skills/manage-ignore/README.md +3 -3
  42. package/skills/manage-ignore/SKILL.md +1 -2
  43. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  44. package/skills/manage-scenario-bridge/README.md +3 -3
  45. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  46. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  47. package/skills/manage-spec-anchors/README.md +3 -3
  48. package/skills/manage-spec-anchors/SKILL.md +1 -1
  49. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  50. package/skills/mission-graph/README.md +2 -2
  51. package/skills/mission-graph/SKILL.md +1 -1
  52. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  53. package/skills/pause-mission/SKILL.md +1 -1
  54. package/skills/place-node/SKILL.md +2 -2
  55. package/skills/place-node/scripts/place-node.mjs +117 -12
  56. package/skills/plan-retirement/README.md +2 -2
  57. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  58. package/skills/resolve-governances/README.md +3 -3
  59. package/skills/resolve-governances/SKILL.md +1 -1
  60. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  61. package/skills/resolve-tracking/SKILL.md +1 -1
  62. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  63. package/skills/scaffold-project-spec/SKILL.md +1 -1
  64. package/skills/spec-gate/README.md +12 -9
  65. package/skills/spec-gate/SKILL.md +2 -2
  66. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  67. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  68. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  69. package/skills/spec-producer-governance/SKILL.md +2 -2
  70. package/skills/start-mission/SKILL.md +1 -1
  71. package/skills/suite-format-governance/README.md +1 -1
  72. package/skills/suite-format-governance/SKILL.md +1 -1
  73. package/skills/touch-set-correction/README.md +5 -5
  74. package/skills/touch-set-correction/SKILL.md +1 -1
  75. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  76. package/skills/verify-scenarios/README.md +6 -6
  77. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  78. package/.plugin/pins.json +0 -3
  79. package/.plugin/plugin.json +0 -19
  80. package/dist/align-spec/scripts/align-spec.js +0 -102
  81. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  82. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  83. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  84. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  85. package/dist/concept-index/scripts/concept-index.js +0 -181
  86. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  87. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  88. package/dist/spec-gate/scripts/check-suite.js +0 -317
  89. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  90. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  91. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  92. package/skills/align-spec/scripts/align-spec.mts +0 -188
  93. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  94. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  95. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  96. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  97. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  98. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  99. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  100. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  102. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  103. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  104. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  105. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  106. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  108. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  109. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  110. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  111. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  112. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  113. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  114. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  115. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  116. package/skills/concept-index/scripts/concept-index.mts +0 -246
  117. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  118. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  119. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  120. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  121. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  124. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  126. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  128. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  129. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  130. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  131. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  132. package/skills/place-node/scripts/place-node.mts +0 -158
  133. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  134. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  135. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  136. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  137. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  138. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  139. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  140. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  141. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  142. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  143. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  144. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  145. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  146. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  147. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  148. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  149. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  150. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,518 +0,0 @@
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, realpathSync } from 'node:fs'
23
- import { join } from 'node:path'
24
- import { pathToFileURL } from 'node:url'
25
-
26
- // ─── the closed sets ───────────────────────────────────────────────────────────
27
-
28
- export const ROLE_KEYS = ['spec-producer', 'solution-producer', 'spec-judge', 'impl-producer', 'impl-judge'] as const
29
- export type RoleKey = (typeof ROLE_KEYS)[number]
30
-
31
- export const BAR_KEYS = ['oracle-spec', 'builder-spec', 'builder-impl', 'architect-spec', 'architect-impl'] as const
32
- export type BarKey = (typeof BAR_KEYS)[number]
33
-
34
- export const ACTORS = ['oracle', 'builder', 'architect'] as const
35
- export const GATES = ['spec', 'impl'] as const
36
-
37
- // The resolved-actor bars each role loads (plugin-contract-governance "Which
38
- // governances each role loads"). Only the resolved-actor bars are matched here;
39
- // the FIXED-UNIVERSAL governances (ownership, lifecycle, spec-format,
40
- // suite-format, gate-validation, combat-log) are invariant per role and stay
41
- // declared in each role/agent definition — this matcher does not re-emit them.
42
- export const ROLE_LOADOUT: Record<RoleKey, { bars: BarKey[] }> = {
43
- 'spec-producer': { bars: ['oracle-spec', 'builder-spec'] },
44
- 'solution-producer': { bars: ['architect-spec'] },
45
- 'spec-judge': { bars: ['oracle-spec', 'builder-spec', 'architect-spec'] },
46
- 'impl-producer': { bars: ['builder-impl', 'architect-impl'] },
47
- 'impl-judge': { bars: ['builder-impl', 'architect-impl'] },
48
- }
49
-
50
- // The SDD-default agent per role. A null ref means the conductor runs the role
51
- // INLINE in the main session (spec/solution-producer) or via a generic spawned
52
- // builder (impl-producer); both are recorded produced-by sdd:sdd-automaton. The two
53
- // judges are spawned cold by name.
54
- export const SDD_DEFAULT_AGENT: Record<RoleKey, string | null> = {
55
- 'spec-producer': null,
56
- 'solution-producer': null,
57
- 'spec-judge': 'sdd-spec-judge',
58
- 'impl-producer': null,
59
- 'impl-judge': 'sdd-impl-judge',
60
- }
61
-
62
- // ─── types ──────────────────────────────────────────────────────────────────────
63
-
64
- // A squad serves a SET of artifact-types with one production chain. A plugin lists
65
- // one or more squads (specialists-and-squads.md "Registry SHAPE"); a type appears
66
- // in at most one squad per plugin.
67
- export interface Squad {
68
- 'artifact-types': string[]
69
- roles: Partial<Record<RoleKey, string | null>>
70
- governances: Partial<Record<BarKey, string | null>>
71
- }
72
-
73
- export interface RegistryEntry {
74
- name: string
75
- version?: string
76
- squads: Squad[]
77
- }
78
-
79
- export interface Registry {
80
- 'sdd-plugins': RegistryEntry[]
81
- }
82
-
83
- // A matched squad carries its owning plugin name (for `<plugin>:<bar>` /
84
- // `<plugin>-<role>` refs).
85
- export interface SquadMatch {
86
- plugin: string
87
- squad: Squad
88
- }
89
-
90
- export interface GovMetadata {
91
- artifactType: string | null
92
- actor: string | null
93
- gate: string | null
94
- compose: 'union' | 'replace' // parsed but NOT emitted — the agent reads it from the loaded file
95
- }
96
-
97
- // The two project tiers a candidate can come from. project = the file's own
98
- // project (most specific); project-root = the outer shared layer in a monorepo.
99
- export type Tier = 'project' | 'project-root'
100
-
101
- export interface GovCandidate extends GovMetadata {
102
- tier: Tier
103
- path: string // root-relative file path (direct-read ref)
104
- }
105
-
106
- // One bar's matched candidates, BUCKETED BY TIER — never ordered, never composed.
107
- // project / project-root are direct-read file paths; plugin / sdd are harness-load
108
- // skill refs. The agent applies precedence sdd < plugin < project-root < project.
109
- export interface BarPlan {
110
- key: BarKey
111
- candidates: {
112
- project: string[]
113
- 'project-root': string[]
114
- plugin: string | null // <plugin>:<bar> harness-load ref, or null when no squad bar
115
- sdd: string // sdd:<key>-governance harness-load ref (always present)
116
- }
117
- }
118
-
119
- export interface AgentResolution {
120
- source: 'plugin' | 'sdd'
121
- ref: string | null // agent name; null = inline (producer) / generic builder (impl-producer)
122
- }
123
-
124
- export interface RolePlan {
125
- role: RoleKey
126
- agent: AgentResolution
127
- bars: BarPlan[] // resolved-actor bars only; fixed-universal live in the role/agent def
128
- }
129
-
130
- export interface LoadPlan {
131
- artifactType: string | null
132
- status: 'complete' | 'needs-input'
133
- plugin: string | null
134
- ambiguous: string[] // plugin names, when status is needs-input
135
- roles: RolePlan[]
136
- }
137
-
138
- // ─── registry parse + migrate-on-read ───────────────────────────────────────────
139
-
140
- // Within a squad: rename the legacy plan-producer role key to solution-producer
141
- // and expand the flat governances{oracle,builder,architect} to the Model-B
142
- // (actor,gate) keys.
143
- function migrateRoles(rolesIn: Record<string, string | null> | undefined): Partial<Record<RoleKey, string | null>> {
144
- const roles = { ...(rolesIn ?? {}) }
145
- if ('plan-producer' in roles) {
146
- const legacy = roles['plan-producer']
147
- if (!('solution-producer' in roles)) roles['solution-producer'] = legacy
148
- delete roles['plan-producer']
149
- }
150
- return roles as Partial<Record<RoleKey, string | null>>
151
- }
152
-
153
- function migrateGovernances(gIn: Record<string, string | null> | undefined): Partial<Record<BarKey, string | null>> {
154
- const g = (gIn ?? {}) as Record<string, string | null>
155
- const isLegacy = 'oracle' in g || 'builder' in g || 'architect' in g
156
- return isLegacy
157
- ? {
158
- 'oracle-spec': g.oracle ?? null,
159
- 'builder-spec': g.builder ?? null,
160
- 'builder-impl': g.builder ?? null,
161
- 'architect-spec': g.architect ?? null,
162
- 'architect-impl': g.architect ?? null,
163
- }
164
- : { ...(g as Partial<Record<BarKey, string | null>>) }
165
- }
166
-
167
- function migrateSquad(sq: Record<string, unknown>): Squad {
168
- const types = (sq['artifact-types'] ?? sq.domains ?? []) as string[]
169
- return {
170
- 'artifact-types': Array.isArray(types) ? types : [],
171
- roles: migrateRoles(sq.roles as Record<string, string | null>),
172
- governances: migrateGovernances(sq.governances as Record<string, string | null>),
173
- }
174
- }
175
-
176
- // Migrate a registry entry on read. The legacy shape carried `domains[]` + a
177
- // shared `roles{}` + `governances{}` directly on the entry; fold it into one
178
- // squad. A live registry still on the legacy shape is migrated on encounter
179
- // (plugin-contract-governance).
180
- export function migrateEntry(entry: Record<string, unknown>): RegistryEntry {
181
- const squads = Array.isArray(entry.squads)
182
- ? (entry.squads as Record<string, unknown>[]).map(migrateSquad)
183
- : [migrateSquad({ 'artifact-types': entry.domains, roles: entry.roles, governances: entry.governances })]
184
- return { name: entry.name as string, version: entry.version as string | undefined, squads }
185
- }
186
-
187
- export function parseRegistry(text: string): Registry {
188
- let raw: unknown
189
- try {
190
- raw = JSON.parse(text)
191
- } catch (e) {
192
- throw new Error(`registry is not valid JSON: ${(e as Error).message}`)
193
- }
194
- if (!raw || typeof raw !== 'object' || !Array.isArray((raw as { 'sdd-plugins': unknown })['sdd-plugins']))
195
- throw new Error('registry has no sdd-plugins array')
196
- const entries = (raw as { 'sdd-plugins': Record<string, unknown>[] })['sdd-plugins'].map(migrateEntry)
197
- return { 'sdd-plugins': entries }
198
- }
199
-
200
- // A project anchor with no registry resolves to all-SDD-defaults — a missing file
201
- // is legal, not an error.
202
- export function loadRegistry(root: string): Registry {
203
- const path = join(root, '.agents', 'universal-plugin.json')
204
- if (!existsSync(path)) return { 'sdd-plugins': [] }
205
- return parseRegistry(readFileSync(path, 'utf8'))
206
- }
207
-
208
- // ─── artifact-type tiebreaker map (.agents/sdd/artifact-types.toml) ──────────────
209
-
210
- // An OPTIONAL, agent-maintained lookup that records resolved path->type bindings
211
- // for the ambiguities convention can't settle (the artifact-type model). It is
212
- // NOT the primary classifier — convention is, and that is the conductor's
213
- // judgment; this table is consulted only on a known ambiguity or a user-flagged
214
- // path. A flat TOML map "<path-or-glob>" = "<artifact-type>"; most-specific glob
215
- // wins. No TOML dependency — the table is intentionally flat key=value.
216
- export interface TypeBinding {
217
- glob: string
218
- type: string
219
- }
220
-
221
- export function parseArtifactTypeMap(text: string): TypeBinding[] {
222
- const out: TypeBinding[] = []
223
- for (const raw of text.split('\n')) {
224
- const line = raw.trim()
225
- if (!line || line.startsWith('#') || line.startsWith('[')) continue // blanks, comments, [section] headers
226
- const m = /^(?:"([^"]+)"|'([^']+)'|(\S+))\s*=\s*(?:"([^"]+)"|'([^']+)')\s*(?:#.*)?$/.exec(line)
227
- if (!m) continue
228
- const glob = m[1] ?? m[2] ?? m[3]
229
- const type = m[4] ?? m[5]
230
- if (glob && type) out.push({ glob, type })
231
- }
232
- return out
233
- }
234
-
235
- // Compile a glob to a full-match RegExp: ** spans path separators, * does not.
236
- function globToRegExp(glob: string): RegExp {
237
- const re = glob
238
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex specials (leave * ?)
239
- .replace(/\*+/g, (m) => (m.length > 1 ? '.*' : '[^/]*')) // ** spans separators, * does not
240
- .replace(/\?/g, '.')
241
- return new RegExp(`^${re}$`)
242
- }
243
-
244
- // Resolve a file path to an artifact-type via the tiebreaker map: the
245
- // most-specific matching glob wins (most literal characters, then longest
246
- // pattern). Returns null when nothing matches — the caller falls back to
247
- // convention.
248
- export function resolveArtifactTypeFromMap(map: TypeBinding[], path: string): string | null {
249
- const matches = map.filter((b) => globToRegExp(b.glob).test(path))
250
- if (matches.length === 0) return null
251
- const literal = (g: string) => g.replace(/[*?]/g, '').length
252
- matches.sort((a, b) => literal(b.glob) - literal(a.glob) || b.glob.length - a.glob.length)
253
- return matches[0].type
254
- }
255
-
256
- // Load the optional tiebreaker map. A missing file is legal (most projects need
257
- // none) — returns an empty table.
258
- export function loadArtifactTypeMap(root: string): TypeBinding[] {
259
- const path = join(root, '.agents', 'sdd', 'artifact-types.toml')
260
- if (!existsSync(path)) return []
261
- return parseArtifactTypeMap(readFileSync(path, 'utf8'))
262
- }
263
-
264
- // ─── project governance discovery ───────────────────────────────────────────────
265
-
266
- function frontmatter(text: string): string[] {
267
- const m = /^---\n([\s\S]*?)\n---/.exec(text)
268
- return m ? m[1].split('\n') : []
269
- }
270
-
271
- const unquote = (s: string) => s.trim().replace(/^["']|["']$/g, '')
272
-
273
- // Read metadata{artifact-type?, actor, gate, compose} from a governance file's
274
- // frontmatter. Returns null when there is no metadata block with an actor+gate —
275
- // a plain doc that is not a resolvable bar.
276
- export function parseGovernanceFrontmatter(text: string): GovMetadata | null {
277
- const lines = frontmatter(text)
278
- let inMeta = false
279
- let artifactType: string | null = null
280
- let actor: string | null = null
281
- let gate: string | null = null
282
- let compose: 'union' | 'replace' = 'union'
283
-
284
- for (const line of lines) {
285
- if (/^metadata:\s*$/.test(line)) {
286
- inMeta = true
287
- continue
288
- }
289
- if (inMeta && /^\S/.test(line)) inMeta = false // dedent ends the block
290
- if (!inMeta) continue
291
- const at = /^\s+artifact-type:\s*(.+)$/.exec(line)
292
- if (at) artifactType = unquote(at[1])
293
- const ac = /^\s+actor:\s*(.+)$/.exec(line)
294
- if (ac) actor = unquote(ac[1])
295
- const ga = /^\s+gate:\s*(.+)$/.exec(line)
296
- if (ga) gate = unquote(ga[1])
297
- const co = /^\s+compose:\s*(.+)$/.exec(line)
298
- if (co) compose = unquote(co[1]) === 'replace' ? 'replace' : 'union'
299
- }
300
-
301
- if (!actor || !gate) return null
302
- return { artifactType, actor, gate, compose }
303
- }
304
-
305
- // Collect project governance candidates from the CALLER-PASSED anchors — never a
306
- // tree walk. Each anchor names a tier and a root; the candidate carries its tier
307
- // (for the agent's precedence) and its root-relative path (the direct-read ref).
308
- // A missing `.agents/governances/` at an anchor contributes nothing. Both .md and
309
- // SKILL.md are read in place — no build.
310
- export function collectAnchorGovernances(anchors: { tier: Tier; root: string }[]): GovCandidate[] {
311
- const out: GovCandidate[] = []
312
- for (const { tier, root } of anchors) {
313
- const dir = join(root, '.agents', 'governances')
314
- let entries: Dirent[]
315
- try {
316
- entries = readdirSync(dir, { withFileTypes: true })
317
- } catch {
318
- continue
319
- }
320
- for (const e of entries) {
321
- if (!e.isFile() || !e.name.endsWith('.md')) continue
322
- const meta = parseGovernanceFrontmatter(readFileSync(join(dir, e.name), 'utf8'))
323
- if (meta) out.push({ ...meta, tier, path: join(root, '.agents', 'governances', e.name) })
324
- }
325
- }
326
- return out
327
- }
328
-
329
- // ─── plugin matching ────────────────────────────────────────────────────────────
330
-
331
- // Match an artifact-type against each plugin's squads: the squad whose
332
- // artifact-types contains it serves the file. Zero matches → all SDD defaults;
333
- // one → that squad; two or more distinct plugins → ambiguous (the conductor
334
- // consults the contested-type choice or asks).
335
- export function matchSquad(
336
- registry: Registry,
337
- artifactType: string | null,
338
- ): { match: SquadMatch | null; ambiguous: string[] } {
339
- if (!artifactType) return { match: null, ambiguous: [] }
340
- const matches: SquadMatch[] = []
341
- for (const p of registry['sdd-plugins'])
342
- for (const sq of p.squads ?? [])
343
- if (sq['artifact-types']?.includes(artifactType)) matches.push({ plugin: p.name, squad: sq })
344
- if (matches.length === 0) return { match: null, ambiguous: [] }
345
- if (matches.length === 1) return { match: matches[0], ambiguous: [] }
346
- return { match: null, ambiguous: [...new Set(matches.map((m) => m.plugin))] }
347
- }
348
-
349
- // ─── bar matching ───────────────────────────────────────────────────────────────
350
-
351
- // Match one (actor, gate) bar across the sources and return the candidates
352
- // BUCKETED BY TIER — no ordering, no compose collapse. A project candidate matches
353
- // when its frontmatter (actor, gate) matches and its artifact-type is the file's
354
- // type OR typeless (a typeless project bar applies to every type). The agent reads
355
- // each candidate's own `compose` at load time and composes by precedence
356
- // (sdd-default < plugin < project-root < project).
357
- export function matchBar(
358
- artifactType: string | null,
359
- actor: string,
360
- gate: string,
361
- ctx: { match: SquadMatch | null; projectGovs: GovCandidate[] },
362
- ): BarPlan {
363
- const key = `${actor}-${gate}` as BarKey
364
- const bucket = (tier: Tier) =>
365
- ctx.projectGovs
366
- .filter((c) => c.tier === tier && c.actor === actor && c.gate === gate)
367
- .filter((c) => c.artifactType === artifactType || c.artifactType === null)
368
- .map((c) => c.path)
369
-
370
- const pluginBar = ctx.match?.squad.governances?.[key]
371
- return {
372
- key,
373
- candidates: {
374
- project: bucket('project'),
375
- 'project-root': bucket('project-root'),
376
- plugin: ctx.match && pluginBar ? `${ctx.match.plugin}:${pluginBar}` : null,
377
- sdd: `sdd:${key}-governance`,
378
- },
379
- }
380
- }
381
-
382
- // ─── role + plan resolution ─────────────────────────────────────────────────────
383
-
384
- // Resolve the agent that runs a role: a named plugin delegate, the SDD default,
385
- // or — for a present-but-missing role key — the <plugin>-<role> convention.
386
- export function resolveAgent(role: RoleKey, match: SquadMatch | null): AgentResolution {
387
- if (!match) return { source: 'sdd', ref: SDD_DEFAULT_AGENT[role] }
388
- if (role in match.squad.roles) {
389
- const named = match.squad.roles[role]
390
- if (named) return { source: 'plugin', ref: named }
391
- return { source: 'sdd', ref: SDD_DEFAULT_AGENT[role] } // explicit null = SDD default
392
- }
393
- return { source: 'plugin', ref: `${match.plugin}-${role}` } // omitted = convention
394
- }
395
-
396
- export function resolveRole(
397
- role: RoleKey,
398
- artifactType: string | null,
399
- ctx: { match: SquadMatch | null; projectGovs: GovCandidate[] },
400
- ): RolePlan {
401
- const bars = ROLE_LOADOUT[role].bars.map((b) => {
402
- const [actor, gate] = b.split('-')
403
- return matchBar(artifactType, actor, gate, ctx)
404
- })
405
- return { role, agent: resolveAgent(role, ctx.match), bars }
406
- }
407
-
408
- export function buildLoadPlan(artifactType: string | null, registry: Registry, projectGovs: GovCandidate[]): LoadPlan {
409
- const { match, ambiguous } = matchSquad(registry, artifactType)
410
- if (ambiguous.length > 0)
411
- return {
412
- artifactType,
413
- status: 'needs-input',
414
- plugin: null,
415
- ambiguous,
416
- roles: [],
417
- }
418
- const ctx = { match, projectGovs }
419
- return {
420
- artifactType,
421
- status: 'complete',
422
- plugin: match?.plugin ?? null,
423
- ambiguous: [],
424
- roles: ROLE_KEYS.map((r) => resolveRole(r, artifactType, ctx)),
425
- }
426
- }
427
-
428
- // ─── registry structural validation (the no-artifact-type CLI gate) ─────────────
429
-
430
- // Validate the registry is well-formed and unambiguous: known role/governance
431
- // keys, a type in at most one squad per plugin, and no artifact-type claimed by
432
- // two plugins (which would force every spec of that type into needs-input).
433
- export function validateRegistry(registry: Registry): string[] {
434
- const v: string[] = []
435
- const typeOwners = new Map<string, string[]>()
436
- for (const entry of registry['sdd-plugins']) {
437
- const where = entry.name || '<unnamed plugin>'
438
- if (!entry.name) v.push('an sdd-plugins entry has no name')
439
- if (!Array.isArray(entry.squads)) {
440
- v.push(`${where}: squads is not an array`)
441
- continue
442
- }
443
- const seenTypes = new Set<string>()
444
- for (const sq of entry.squads) {
445
- if (!Array.isArray(sq['artifact-types'])) v.push(`${where}: a squad's artifact-types is not an array`)
446
- for (const r of Object.keys(sq.roles ?? {}))
447
- if (!ROLE_KEYS.includes(r as RoleKey)) v.push(`${where}: unknown role key "${r}"`)
448
- for (const k of Object.keys(sq.governances ?? {}))
449
- if (!BAR_KEYS.includes(k as BarKey)) v.push(`${where}: unknown governance key "${k}"`)
450
- for (const t of sq['artifact-types'] ?? []) {
451
- if (seenTypes.has(t)) v.push(`${where}: artifact-type "${t}" appears in more than one squad`)
452
- seenTypes.add(t)
453
- }
454
- }
455
- for (const t of seenTypes) typeOwners.set(t, [...(typeOwners.get(t) ?? []), where])
456
- }
457
- for (const [t, owners] of typeOwners)
458
- if (owners.length > 1)
459
- v.push(`artifact-type "${t}" is claimed by ${owners.length} plugins (${owners.join(', ')}) — ambiguous`)
460
- return v
461
- }
462
-
463
- // ─── CLI entry ──────────────────────────────────────────────────────────────────
464
-
465
- export function main(argv: string[]): number {
466
- const root = argv.includes('--root') ? argv[argv.indexOf('--root') + 1] : '.'
467
- const explicitType = argv.includes('--artifact-type') ? argv[argv.indexOf('--artifact-type') + 1] : null
468
- const pathArg = argv.includes('--path') ? argv[argv.indexOf('--path') + 1] : null
469
- // Caller-passed project anchors. --project defaults to --root (single-project);
470
- // --project-root is the outer shared layer in a monorepo (omitted otherwise).
471
- const projectArg = argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : root
472
- const projectRootArg = argv.includes('--project-root') ? argv[argv.indexOf('--project-root') + 1] : null
473
- const anchors: { tier: Tier; root: string }[] = [{ tier: 'project', root: projectArg }]
474
- if (projectRootArg) anchors.push({ tier: 'project-root', root: projectRootArg })
475
-
476
- // --artifact-type wins (explicit override); else --path consults the optional
477
- // tiebreaker map (most-specific glob). A --path with no map match stays null —
478
- // the caller falls back to convention, it is not a registry-validation request.
479
- let artifactType = explicitType
480
- if (!artifactType && pathArg) artifactType = resolveArtifactTypeFromMap(loadArtifactTypeMap(root), pathArg)
481
-
482
- let registry: Registry
483
- try {
484
- registry = loadRegistry(root)
485
- } catch (e) {
486
- console.error(`✗ ${(e as Error).message}`)
487
- return 1
488
- }
489
-
490
- if (!artifactType && pathArg) {
491
- process.stdout.write(
492
- `${JSON.stringify({ path: pathArg, artifactType: null, note: 'no tiebreaker match — classify by convention' }, null, 2)}\n`,
493
- )
494
- return 0
495
- }
496
-
497
- // No artifact-type and no path → validate the registry is well-formed + unambiguous.
498
- if (!artifactType) {
499
- const violations = validateRegistry(registry)
500
- if (violations.length) {
501
- for (const line of violations) console.error(`✗ ${line}`)
502
- return 1
503
- }
504
- process.stdout.write('governance registry OK\n')
505
- return 0
506
- }
507
-
508
- // With an artifact-type → emit the per-role plan (agent + tier-bucketed bars).
509
- const plan = buildLoadPlan(artifactType, registry, collectAnchorGovernances(anchors))
510
- process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`)
511
- if (plan.status === 'needs-input')
512
- console.error(`needs-input: artifact-type "${artifactType}" is claimed by ${plan.ambiguous.join(', ')}`)
513
- return 0
514
- }
515
-
516
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
517
- process.exit(main(process.argv.slice(2)))
518
- }
@@ -1,150 +0,0 @@
1
- import { existsSync, readFileSync, realpathSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- //#region skills/resolve-tracking/scripts/resolve-tracking.mts
5
- function globToRegExpBody(glob) {
6
- return glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*+/g, (m) => m.length > 1 ? ".*" : "[^/]*").replace(/\?/g, "[^/]");
7
- }
8
- function parseIgnoreLine(raw) {
9
- const trimmed = raw.replace(/\r$/, "").trim();
10
- if (!trimmed || trimmed.startsWith("#")) return "skip";
11
- let body = trimmed;
12
- const negated = body.startsWith("!");
13
- if (negated) body = body.slice(1);
14
- if (body.endsWith("/")) body = body.slice(0, -1);
15
- let anchored = false;
16
- if (body.startsWith("/")) {
17
- anchored = true;
18
- body = body.slice(1);
19
- } else if (body.includes("/")) anchored = true;
20
- if (body === "") return null;
21
- const prefix = anchored ? "^" : "^(?:.*/)?";
22
- const core = globToRegExpBody(body);
23
- const suffix = "(?:/.*)?$";
24
- try {
25
- return {
26
- negated,
27
- regex: new RegExp(prefix + core + suffix),
28
- source: trimmed
29
- };
30
- } catch {
31
- return null;
32
- }
33
- }
34
- function parseIgnoreFile(text) {
35
- const out = [];
36
- for (const raw of text.split("\n")) {
37
- const rule = parseIgnoreLine(raw);
38
- if (rule && rule !== "skip") out.push(rule);
39
- }
40
- return out;
41
- }
42
- function loadIgnoreRules(root) {
43
- const path = join(root, ".agents", "sdd", ".sddignore");
44
- if (!existsSync(path)) return [];
45
- return parseIgnoreFile(readFileSync(path, "utf8"));
46
- }
47
- function resolveFromIgnore(rules, path) {
48
- let value = null;
49
- for (const rule of rules) if (rule.regex.test(path)) value = rule.negated ? "tracked" : "ignored";
50
- return value;
51
- }
52
- const KIND_DEFAULT_GLOBS = {
53
- skill: {
54
- ignored: [".agents/skills/**"],
55
- tracked: [
56
- "skills/**",
57
- "plugins/*/skills/**",
58
- "packages/*/skills/**"
59
- ]
60
- },
61
- subagent: {
62
- ignored: [".agents/agents/**"],
63
- tracked: ["plugins/*/agents/**", "packages/*/agents/**"]
64
- },
65
- command: {
66
- ignored: [".agents/commands/**"],
67
- tracked: ["plugins/*/commands/**", "packages/*/commands/**"]
68
- }
69
- };
70
- function globMatch(glob, path) {
71
- return new RegExp(`^${globToRegExpBody(glob)}$`).test(path);
72
- }
73
- function resolveKindDefault(artifactType, path) {
74
- if (!artifactType) return null;
75
- const kind = KIND_DEFAULT_GLOBS[artifactType];
76
- if (!kind) return null;
77
- if (kind.ignored.some((g) => globMatch(g, path))) return "ignored";
78
- if (kind.tracked.some((g) => globMatch(g, path))) return "tracked";
79
- return null;
80
- }
81
- function resolveTracking(input) {
82
- if (input.explicit) return {
83
- value: input.explicit,
84
- reason: "explicit override"
85
- };
86
- const ignoreMatch = resolveFromIgnore(loadIgnoreRules(input.root), input.path);
87
- if (ignoreMatch) return {
88
- value: ignoreMatch,
89
- reason: ".sddignore"
90
- };
91
- const kindDefault = resolveKindDefault(input.artifactType, input.path);
92
- if (kindDefault) return {
93
- value: kindDefault,
94
- reason: `kind-default (${input.artifactType})`
95
- };
96
- return {
97
- value: "tracked",
98
- reason: "fail-closed (no signal)"
99
- };
100
- }
101
- function validateIgnoreFile(root) {
102
- const path = join(root, ".agents", "sdd", ".sddignore");
103
- if (!existsSync(path)) return {
104
- ok: true,
105
- message: ".sddignore OK (absent, fine)"
106
- };
107
- const text = readFileSync(path, "utf8");
108
- const notes = [];
109
- text.split("\n").forEach((raw, i) => {
110
- if (parseIgnoreLine(raw) === null) notes.push(`line ${i + 1}: not a valid gitignore pattern: ${raw.trim()}`);
111
- });
112
- if (notes.length > 0) return {
113
- ok: false,
114
- message: notes.join("\n")
115
- };
116
- return {
117
- ok: true,
118
- message: `.sddignore OK (${parseIgnoreFile(text).length} rule(s))`
119
- };
120
- }
121
- function parseArgs(argv) {
122
- const out = {};
123
- for (let i = 0; i < argv.length; i++) if (argv[i].startsWith("--")) {
124
- out[argv[i].slice(2)] = argv[i + 1];
125
- i++;
126
- }
127
- return out;
128
- }
129
- function main(argv) {
130
- const args = parseArgs(argv);
131
- const root = args.root ?? ".";
132
- if (!args.path) {
133
- const result = validateIgnoreFile(root);
134
- process.stdout.write(`${result.message}\n`);
135
- if (!result.ok) process.exitCode = 1;
136
- return;
137
- }
138
- const explicit = args.explicit === "tracked" || args.explicit === "ignored" ? args.explicit : void 0;
139
- const result = resolveTracking({
140
- root,
141
- path: args.path,
142
- artifactType: args["artifact-type"],
143
- explicit
144
- });
145
- process.stdout.write(`${result.value}\n`);
146
- process.stdout.write(`reason: ${result.reason}\n`);
147
- }
148
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) main(process.argv.slice(2));
149
- //#endregion
150
- export { loadIgnoreRules, main, parseIgnoreFile, parseIgnoreLine, resolveFromIgnore, resolveKindDefault, resolveTracking, validateIgnoreFile };