cyber-sdd 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +17 -0
- package/.codex-plugin/plugin.json +17 -0
- package/.plugin/plugin.json +17 -0
- package/README.md +159 -0
- package/agents/sdd-automaton.md +97 -0
- package/agents/sdd-impl-judge.md +214 -0
- package/agents/sdd-scanner.md +120 -0
- package/agents/sdd-spec-judge.md +224 -0
- package/agents/sdd-warden.md +101 -0
- package/package.json +24 -0
- package/skills/align-spec/README.md +20 -0
- package/skills/align-spec/SKILL.md +111 -0
- package/skills/align-spec/scripts/align-spec.mts +187 -0
- package/skills/architect-impl-governance/README.md +46 -0
- package/skills/architect-impl-governance/SKILL.md +45 -0
- package/skills/architect-spec-governance/README.md +48 -0
- package/skills/architect-spec-governance/SKILL.md +59 -0
- package/skills/blast-estimate/README.md +47 -0
- package/skills/blast-estimate/SKILL.md +133 -0
- package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
- package/skills/builder-impl-governance/README.md +47 -0
- package/skills/builder-impl-governance/SKILL.md +47 -0
- package/skills/builder-spec-governance/README.md +49 -0
- package/skills/builder-spec-governance/SKILL.md +36 -0
- package/skills/check-partition-quality/README.md +22 -0
- package/skills/check-partition-quality/SKILL.md +51 -0
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
- package/skills/check-plan-safety/README.md +17 -0
- package/skills/check-plan-safety/SKILL.md +60 -0
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
- package/skills/check-project-specs/README.md +19 -0
- package/skills/check-project-specs/SKILL.md +69 -0
- package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
- package/skills/check-scenario-overlap/README.md +19 -0
- package/skills/check-scenario-overlap/SKILL.md +74 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
- package/skills/check-spec-structure/README.md +17 -0
- package/skills/check-spec-structure/SKILL.md +66 -0
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
- package/skills/collision-ladder/README.md +18 -0
- package/skills/collision-ladder/SKILL.md +83 -0
- package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
- package/skills/combat-log-governance/README.md +13 -0
- package/skills/combat-log-governance/SKILL.md +257 -0
- package/skills/concept-index/README.md +13 -0
- package/skills/concept-index/SKILL.md +38 -0
- package/skills/concept-index/scripts/concept-index.mts +245 -0
- package/skills/discover-plans/README.md +16 -0
- package/skills/discover-plans/SKILL.md +74 -0
- package/skills/discover-plans/scripts/discover-plans.mts +212 -0
- package/skills/discover-specs/README.md +15 -0
- package/skills/discover-specs/SKILL.md +76 -0
- package/skills/discover-specs/scripts/discover-specs.mts +396 -0
- package/skills/doctrine-loop/README.md +15 -0
- package/skills/doctrine-loop/SKILL.md +97 -0
- package/skills/formation-loop/README.md +17 -0
- package/skills/formation-loop/SKILL.md +140 -0
- package/skills/gate-validation-governance/README.md +12 -0
- package/skills/gate-validation-governance/SKILL.md +87 -0
- package/skills/impl-producer-governance/README.md +48 -0
- package/skills/impl-producer-governance/SKILL.md +85 -0
- package/skills/init/README.md +27 -0
- package/skills/init/SKILL.md +68 -0
- package/skills/init/scripts/wire-statusline.mts +276 -0
- package/skills/lifecycle-governance/README.md +11 -0
- package/skills/lifecycle-governance/SKILL.md +168 -0
- package/skills/manage/README.md +9 -0
- package/skills/manage/SKILL.md +62 -0
- package/skills/manage-ignore/README.md +19 -0
- package/skills/manage-ignore/SKILL.md +52 -0
- package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
- package/skills/manage-scenario-bridge/README.md +20 -0
- package/skills/manage-scenario-bridge/SKILL.md +60 -0
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
- package/skills/manage-spec-anchors/README.md +18 -0
- package/skills/manage-spec-anchors/SKILL.md +56 -0
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
- package/skills/mission-graph/README.md +15 -0
- package/skills/mission-graph/SKILL.md +67 -0
- package/skills/mission-graph/scripts/mission-graph.mts +844 -0
- package/skills/oracle-spec-governance/README.md +45 -0
- package/skills/oracle-spec-governance/SKILL.md +45 -0
- package/skills/ownership-governance/README.md +65 -0
- package/skills/ownership-governance/SKILL.md +104 -0
- package/skills/pause-mission/README.md +18 -0
- package/skills/pause-mission/SKILL.md +112 -0
- package/skills/place-node/README.md +12 -0
- package/skills/place-node/SKILL.md +47 -0
- package/skills/place-node/scripts/place-node.mts +157 -0
- package/skills/plan-retirement/README.md +32 -0
- package/skills/plan-retirement/SKILL.md +90 -0
- package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
- package/skills/plugin-contract-governance/README.md +12 -0
- package/skills/plugin-contract-governance/SKILL.md +112 -0
- package/skills/remediation-governance/README.md +46 -0
- package/skills/remediation-governance/SKILL.md +78 -0
- package/skills/resolve-governances/README.md +18 -0
- package/skills/resolve-governances/SKILL.md +50 -0
- package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
- package/skills/resolve-tracking/SKILL.md +64 -0
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
- package/skills/resume-mission/README.md +12 -0
- package/skills/resume-mission/SKILL.md +53 -0
- package/skills/scaffold-project-spec/README.md +7 -0
- package/skills/scaffold-project-spec/SKILL.md +192 -0
- package/skills/sdd/README.md +7 -0
- package/skills/sdd/SKILL.md +92 -0
- package/skills/solution-producer-governance/README.md +9 -0
- package/skills/solution-producer-governance/SKILL.md +44 -0
- package/skills/spec-format-governance/README.md +73 -0
- package/skills/spec-format-governance/SKILL.md +114 -0
- package/skills/spec-gate/README.md +26 -0
- package/skills/spec-gate/SKILL.md +201 -0
- package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
- package/skills/spec-gate/scripts/check-suite.mts +501 -0
- package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
- package/skills/spec-producer-governance/README.md +7 -0
- package/skills/spec-producer-governance/SKILL.md +86 -0
- package/skills/spec-structure-governance/README.md +40 -0
- package/skills/spec-structure-governance/SKILL.md +169 -0
- package/skills/ssa-lowering/README.md +26 -0
- package/skills/ssa-lowering/SKILL.md +181 -0
- package/skills/start-mission/README.md +7 -0
- package/skills/start-mission/SKILL.md +115 -0
- package/skills/suite-format-governance/README.md +75 -0
- package/skills/suite-format-governance/SKILL.md +299 -0
- package/skills/suite-format-governance/references/rubric.md +313 -0
- package/skills/touch-set-correction/README.md +16 -0
- package/skills/touch-set-correction/SKILL.md +67 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
- package/skills/verify-scenarios/README.md +17 -0
- package/skills/verify-scenarios/SKILL.md +109 -0
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +386 -0
|
@@ -0,0 +1,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).
|