@kal-elsam/kairo-runtime 0.4.3 → 0.5.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/README.md +124 -17
- package/global-template/components/catalog.json +63 -6
- package/global-template/components/sdd-core/personas/teaching.md +42 -0
- package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
- package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +29 -8
- package/scripts/smoke-test.sh +9 -0
- package/src/cli.js +54 -5
- package/src/global/action-planner.js +17 -4
- package/src/global/component-authoring.js +1 -2
- package/src/global/component-ecosystem-checks.js +74 -15
- package/src/global/component-health.js +56 -0
- package/src/global/component-integration-cli.js +302 -0
- package/src/global/component-manifest.js +214 -0
- package/src/global/component-registry.js +19 -2
- package/src/global/component-resolve.js +51 -0
- package/src/global/components/sdd-core.js +6 -0
- package/src/global/control-plane-proposals.js +229 -0
- package/src/global/control-plane-snapshot.js +212 -0
- package/src/global/dashboard-guidance.js +27 -3
- package/src/global/diff.js +2 -5
- package/src/global/global-cli.js +16 -1
- package/src/global/global-doctor.js +75 -2
- package/src/global/global-installer.js +43 -6
- package/src/global/governance-actions.js +212 -0
- package/src/global/governance-repair.js +13 -0
- package/src/global/ink/cockpit-changes.js +124 -0
- package/src/global/ink/cockpit-control-center.js +106 -0
- package/src/global/ink/cockpit-controller.js +17 -0
- package/src/global/ink/cockpit-enter.js +27 -0
- package/src/global/ink/cockpit-focus.js +7 -1
- package/src/global/ink/cockpit-models.js +98 -46
- package/src/global/ink/cockpit-proposals.js +82 -0
- package/src/global/ink/cockpit-recovery.js +120 -0
- package/src/global/ink/cockpit-runs.js +47 -0
- package/src/global/ink/cockpit-scan.js +60 -0
- package/src/global/ink/cockpit-views.js +144 -74
- package/src/global/ink/orchestrator-app.js +156 -42
- package/src/global/ink/orchestrator-state.js +6 -0
- package/src/global/ink/use-orchestrator-data.js +239 -17
- package/src/global/integrations/engram-apply.js +105 -0
- package/src/global/integrations/engram-evidence.js +218 -0
- package/src/global/integrations/engram-exec.js +107 -0
- package/src/global/integrations/engram-plan.js +60 -0
- package/src/global/integrations/engram-provider.js +22 -0
- package/src/global/integrations/engram-receipts.js +116 -0
- package/src/global/integrations/engram-rollback.js +131 -0
- package/src/global/integrations/index.js +12 -0
- package/src/global/integrations/provider-contract.js +83 -0
- package/src/global/integrations/provider-registry.js +47 -0
- package/src/global/integrations/sdd-apply.js +190 -0
- package/src/global/integrations/sdd-destinations.js +95 -0
- package/src/global/integrations/sdd-evidence.js +89 -0
- package/src/global/integrations/sdd-fs-guard.js +183 -0
- package/src/global/integrations/sdd-lifecycle.js +84 -0
- package/src/global/integrations/sdd-persona.js +71 -0
- package/src/global/integrations/sdd-plan.js +87 -0
- package/src/global/integrations/sdd-provider.js +21 -0
- package/src/global/integrations/sdd-receipts.js +44 -0
- package/src/global/integrations/sdd-rollback.js +269 -0
- package/src/global/integrations/sdd-skill-files.js +61 -0
- package/src/global/integrations/sdd-state.js +175 -0
- package/src/global/integrations/sdd-verify.js +122 -0
- package/src/global/intelligence/backends/opencode-catalog.js +212 -0
- package/src/global/intelligence/backends/opencode-cli.js +247 -0
- package/src/global/intelligence/backends/opencode-evidence.js +129 -0
- package/src/global/intelligence/backends/opencode-http.js +152 -0
- package/src/global/intelligence/backends/opencode-invoke.js +83 -0
- package/src/global/intelligence/backends/opencode-providers.js +67 -0
- package/src/global/intelligence/backends/opencode-runtime.js +242 -0
- package/src/global/intelligence/context-compiler.js +95 -50
- package/src/global/intelligence/index.js +38 -1
- package/src/global/intelligence/orchestrate.js +4 -2
- package/src/global/intelligence/registry.js +62 -6
- package/src/global/intelligence/router.js +188 -70
- package/src/global/intelligence/transport-registry.js +147 -0
- package/src/global/intelligence/types.js +21 -2
- package/src/global/intelligence-cli.js +48 -17
- package/src/global/intelligence-session.js +37 -0
- package/src/global/json-output.js +1 -0
- package/src/global/load-component-catalog.js +10 -28
- package/src/global/load-workspace-component-catalog.js +22 -63
- package/src/global/state-migration.js +21 -1
- package/src/global/state.js +13 -8
- package/src/global/status.js +4 -17
- package/src/global/sync.js +2 -5
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { resolveHomeDir, harnessHomePaths } from "./paths.js";
|
|
4
|
+
import { readGlobalState, writeGlobalState } from "./state.js";
|
|
5
|
+
import { detectInstalledAdapters } from "./registry.js";
|
|
6
|
+
import { resolveComponent } from "./component-registry.js";
|
|
7
|
+
import { printJson } from "./json-output.js";
|
|
8
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
9
|
+
import { requireIntegrationProvider } from "./integrations/provider-registry.js";
|
|
10
|
+
import { ensureIntegrationProvidersRegistered } from "./integrations/index.js";
|
|
11
|
+
import { ENGRAM_INTEGRATION_STATUS } from "./integrations/engram-evidence.js";
|
|
12
|
+
import { SDD_HEALTH } from "./integrations/sdd-evidence.js";
|
|
13
|
+
import {
|
|
14
|
+
hasSuccessfulSddRollbackMutations, recordSddMaterialization, reconcileSddStateAfterRollback
|
|
15
|
+
} from "./integrations/sdd-state.js";
|
|
16
|
+
import { loadSddReceipt } from "./integrations/sdd-receipts.js";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
19
|
+
|
|
20
|
+
const COMPONENT_PROVIDERS = Object.freeze({
|
|
21
|
+
"engram-memory": "engram",
|
|
22
|
+
"sdd-core": "sdd-core"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export async function assertComponentInstalled(componentId, { homeDir = resolveHomeDir() } = {}) {
|
|
26
|
+
const state = await readGlobalState(harnessHomePaths(homeDir).statePath);
|
|
27
|
+
const installed = (state?.components ?? []).some((entry) => entry.id === componentId);
|
|
28
|
+
if (!installed) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Component "${componentId}" is not installed in Kairo state. Run "${formatCliCommand(`install --components ${componentId}`)}" first.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return resolveComponent(componentId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireConfiguredComponent(componentId) {
|
|
37
|
+
const providerId = COMPONENT_PROVIDERS[componentId];
|
|
38
|
+
if (!providerId) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`components configure/verify/rollback supports: ${Object.keys(COMPONENT_PROVIDERS).join(", ")} (got "${componentId}").`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return providerId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runComponentsConfigure(options) {
|
|
47
|
+
const componentId = options.componentId;
|
|
48
|
+
const providerId = requireConfiguredComponent(componentId);
|
|
49
|
+
ensureIntegrationProvidersRegistered();
|
|
50
|
+
const homeDir = resolveHomeDir();
|
|
51
|
+
await assertComponentInstalled(componentId, { homeDir });
|
|
52
|
+
const provider = options.provider ?? requireIntegrationProvider(providerId);
|
|
53
|
+
const context = await buildProviderContext(options, { homeDir, componentId });
|
|
54
|
+
const result = await provider.apply(context);
|
|
55
|
+
|
|
56
|
+
if (componentId === "sdd-core" && result.receipt && !result.dryRun && !result.cancelled) {
|
|
57
|
+
if (result.receipt.ok || result.receipt.partial) {
|
|
58
|
+
await persistSddReceiptState(homeDir, result.receipt);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (options.json) {
|
|
63
|
+
printJson(result);
|
|
64
|
+
if (isConfigureFailure(result)) process.exitCode = 1;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (componentId === "sdd-core") printSddConfigureHuman(result);
|
|
69
|
+
else printEngramConfigureHuman(result);
|
|
70
|
+
if (isConfigureFailure(result)) process.exitCode = 1;
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runComponentsVerify(options) {
|
|
75
|
+
const componentId = options.componentId;
|
|
76
|
+
const providerId = requireConfiguredComponent(componentId);
|
|
77
|
+
ensureIntegrationProvidersRegistered();
|
|
78
|
+
const homeDir = resolveHomeDir();
|
|
79
|
+
await assertComponentInstalled(componentId, { homeDir });
|
|
80
|
+
const provider = requireIntegrationProvider(providerId);
|
|
81
|
+
const context = await buildProviderContext(options, { homeDir, componentId });
|
|
82
|
+
const result = await provider.verify(context);
|
|
83
|
+
|
|
84
|
+
if (options.json) {
|
|
85
|
+
printJson(result);
|
|
86
|
+
if (result.ok === false) process.exitCode = 1;
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (componentId === "sdd-core") printSddVerifyHuman(result);
|
|
91
|
+
else printEngramConfigureHuman(result);
|
|
92
|
+
if (result.ok === false) process.exitCode = 1;
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runComponentsRollback(options) {
|
|
97
|
+
const componentId = options.componentId;
|
|
98
|
+
const providerId = requireConfiguredComponent(componentId);
|
|
99
|
+
if (!options.receiptId) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Missing receipt id. Use: ${formatCliCommand(`components rollback ${componentId} --receipt <id>`)}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
ensureIntegrationProvidersRegistered();
|
|
106
|
+
const homeDir = resolveHomeDir();
|
|
107
|
+
await assertComponentInstalled(componentId, { homeDir });
|
|
108
|
+
const provider = options.provider ?? requireIntegrationProvider(providerId);
|
|
109
|
+
const state = componentId === "sdd-core"
|
|
110
|
+
? await readGlobalState(harnessHomePaths(homeDir).statePath)
|
|
111
|
+
: null;
|
|
112
|
+
const result = await provider.rollback({
|
|
113
|
+
receiptId: options.receiptId,
|
|
114
|
+
homeDir,
|
|
115
|
+
dryRun: Boolean(options.dryRun),
|
|
116
|
+
yes: Boolean(options.yes),
|
|
117
|
+
json: Boolean(options.json),
|
|
118
|
+
interactive: null,
|
|
119
|
+
personaAgentIds: state?.sdd?.personaAgentIds ?? []
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
componentId === "sdd-core"
|
|
124
|
+
&& !result.dryRun && !result.cancelled && !result.blocked
|
|
125
|
+
&& hasSuccessfulSddRollbackMutations(result.actions)
|
|
126
|
+
) {
|
|
127
|
+
await reconcileSddRollbackState(homeDir, result);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.json) {
|
|
131
|
+
printJson(result);
|
|
132
|
+
if (!result.ok && !result.cancelled) process.exitCode = 1;
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(formatCliCommand(`components rollback ${componentId}`));
|
|
137
|
+
console.log(`Receipt: ${result.receiptId}`);
|
|
138
|
+
if (result.cancelled) {
|
|
139
|
+
console.log("Cancelled.");
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
for (const action of result.actions ?? []) {
|
|
143
|
+
console.log(` ${action.action.padEnd(8)} ${action.path ?? "persona"}${action.reason ? ` — ${action.reason}` : ""}`);
|
|
144
|
+
}
|
|
145
|
+
console.log(result.ok ? "Rollback complete." : "Rollback finished with skips/errors.");
|
|
146
|
+
if (!result.ok) process.exitCode = 1;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function buildProviderContext(options, { homeDir, componentId }) {
|
|
151
|
+
const detectedAgentIds = detectInstalledAdapters({ homeDir });
|
|
152
|
+
const base = {
|
|
153
|
+
requestedAgentIds: options.adapters,
|
|
154
|
+
detectedAgentIds,
|
|
155
|
+
homeDir,
|
|
156
|
+
dryRun: Boolean(options.dryRun),
|
|
157
|
+
yes: Boolean(options.yes),
|
|
158
|
+
json: Boolean(options.json),
|
|
159
|
+
interactive: null
|
|
160
|
+
};
|
|
161
|
+
if (componentId !== "sdd-core") return base;
|
|
162
|
+
|
|
163
|
+
const state = await readGlobalState(harnessHomePaths(homeDir).statePath);
|
|
164
|
+
const trackedFiles = Object.fromEntries(
|
|
165
|
+
(state?.sdd?.files ?? []).map((file) => [file.destinationPath, file.hash])
|
|
166
|
+
);
|
|
167
|
+
return {
|
|
168
|
+
...base,
|
|
169
|
+
packageRoot: options.packageRoot ?? DEFAULT_PACKAGE_ROOT,
|
|
170
|
+
persona: options.persona ?? state?.sdd?.persona ?? "off",
|
|
171
|
+
personaAgentIds: state?.sdd?.personaAgentIds ?? [],
|
|
172
|
+
trackedFiles
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function persistSddReceiptState(homeDir, receipt) {
|
|
177
|
+
const paths = harnessHomePaths(homeDir);
|
|
178
|
+
const state = (await readGlobalState(paths.statePath)) ?? {};
|
|
179
|
+
await writeGlobalState(paths.statePath, recordSddMaterialization(state, { receipt }));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function reconcileSddRollbackState(homeDir, result) {
|
|
183
|
+
const paths = harnessHomePaths(homeDir);
|
|
184
|
+
const state = (await readGlobalState(paths.statePath)) ?? {};
|
|
185
|
+
const receipt = await loadSddReceipt(result.receiptId, { homeDir });
|
|
186
|
+
await writeGlobalState(paths.statePath, reconcileSddStateAfterRollback(state, {
|
|
187
|
+
receipt, actions: result.actions ?? []
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isConfigureFailure(result) {
|
|
192
|
+
return Boolean(result.blocked || (result.receipt && !result.receipt.ok));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function buildEngramIntegrationChecks(inspection) {
|
|
196
|
+
const checks = [];
|
|
197
|
+
const binary = inspection.binary;
|
|
198
|
+
const binaryStatus = binary.status === ENGRAM_INTEGRATION_STATUS.AVAILABLE
|
|
199
|
+
|| binary.status === ENGRAM_INTEGRATION_STATUS.CONFIGURED
|
|
200
|
+
|| binary.status === ENGRAM_INTEGRATION_STATUS.UNCONFIGURED
|
|
201
|
+
? "ok"
|
|
202
|
+
: "warning";
|
|
203
|
+
|
|
204
|
+
checks.push({
|
|
205
|
+
name: "engram:binary",
|
|
206
|
+
status: binaryStatus,
|
|
207
|
+
category: "integration",
|
|
208
|
+
componentId: "engram-memory",
|
|
209
|
+
detail: binary.path
|
|
210
|
+
? `Engram ${binary.version ?? "unknown"} at ${binary.path} (${binary.status}).${binary.guidance ? ` ${binary.guidance}` : ""}`
|
|
211
|
+
: (binary.guidance ?? "Engram binary missing.")
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
for (const agent of inspection.agents ?? []) {
|
|
215
|
+
const status = agent.status === ENGRAM_INTEGRATION_STATUS.CONFIGURED
|
|
216
|
+
? "ok"
|
|
217
|
+
: "warning";
|
|
218
|
+
checks.push({
|
|
219
|
+
name: `engram:agent:${agent.id}`,
|
|
220
|
+
status,
|
|
221
|
+
category: "integration",
|
|
222
|
+
componentId: "engram-memory",
|
|
223
|
+
detail: `${agent.id} → ${agent.slug}: ${agent.status} (config evidence only; not runtime-active).`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return checks;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildSddIntegrationChecks(verification) {
|
|
231
|
+
const summary = verification.summary ?? {};
|
|
232
|
+
const persona = verification.persona ?? {};
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
name: "sdd-core:skills",
|
|
236
|
+
status: verification.status === SDD_HEALTH.CONFIGURED ? "ok" : "warning",
|
|
237
|
+
category: "integration", componentId: "sdd-core",
|
|
238
|
+
detail: `SDD skills ${verification.status}: configured=${summary.configured ?? 0}, missing=${summary.missing ?? 0}, drifted=${summary.drifted ?? 0}, conflict=${summary.conflict ?? 0} (disk presence ≠ runtime active).`
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "sdd-core:persona",
|
|
242
|
+
status: persona.status === "configured" || persona.status === "off" ? "ok" : "warning",
|
|
243
|
+
category: "integration", componentId: "sdd-core",
|
|
244
|
+
detail: `SDD persona ${persona.status ?? "off"}: active=${Boolean(persona.personaActive)} (state alone ≠ runtime active).`
|
|
245
|
+
}
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function printEngramConfigureHuman(result) {
|
|
250
|
+
console.log(formatCliCommand("components configure engram-memory"));
|
|
251
|
+
if (result.binary) {
|
|
252
|
+
console.log(`Binary: ${result.binary.path ?? "missing"} (${result.binary.version ?? "n/a"}, ${result.binary.status})`);
|
|
253
|
+
}
|
|
254
|
+
if (result.guidance) console.log(`Guidance: ${result.guidance}`);
|
|
255
|
+
for (const action of result.actions ?? []) {
|
|
256
|
+
console.log(` ${action.action.padEnd(8)} ${action.agentId} (${action.slug})`);
|
|
257
|
+
}
|
|
258
|
+
if (result.dryRun) {
|
|
259
|
+
console.log("Dry-run only — no setup executed.");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (result.cancelled) {
|
|
263
|
+
console.log("Cancelled.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (result.receipt) {
|
|
267
|
+
console.log(`Receipt: ${result.receipt.id}${result.receipt.partial ? " (partial)" : ""}`);
|
|
268
|
+
console.log(`Status: ${result.receipt.status}`);
|
|
269
|
+
if (result.receipt.status === ENGRAM_INTEGRATION_STATUS.RESTART_REQUIRED) {
|
|
270
|
+
console.log("Restart the configured agents to load MCP. Kairo does not claim runtime reload.");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function printSddConfigureHuman(result) {
|
|
276
|
+
console.log(formatCliCommand("components configure sdd-core"));
|
|
277
|
+
console.log(`Persona: ${result.persona ?? "off"}${result.personaActive ? " (active)" : " (off)"}`);
|
|
278
|
+
const s = result.summary ?? {};
|
|
279
|
+
console.log(`Plan: create=${s.create ?? 0} noop=${s.noop ?? 0} update=${s.update ?? 0} conflict=${s.conflict ?? 0}`);
|
|
280
|
+
for (const action of result.actions ?? []) {
|
|
281
|
+
console.log(` ${action.action.padEnd(8)} ${action.skillId} → ${action.destinationPath}`);
|
|
282
|
+
}
|
|
283
|
+
if (result.dryRun) return void console.log("Dry-run only — no skills materialized.");
|
|
284
|
+
if (result.cancelled) return void console.log("Cancelled.");
|
|
285
|
+
if (result.blocked) return void console.log(`Blocked: ${result.reason ?? "conflicts present"}`);
|
|
286
|
+
if (result.receipt) {
|
|
287
|
+
console.log(`Receipt: ${result.receipt.id}${result.receipt.partial ? " (partial)" : ""}`);
|
|
288
|
+
if (result.sessionRefreshRequired) {
|
|
289
|
+
console.log("session_refresh_required — restart agents to load skills; Kairo does not claim current sessions already loaded them.");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function printSddVerifyHuman(result) {
|
|
295
|
+
console.log(formatCliCommand("components verify sdd-core"));
|
|
296
|
+
const s = result.summary ?? {};
|
|
297
|
+
console.log(`Status: ${result.status}`);
|
|
298
|
+
console.log(`Summary: configured=${s.configured ?? 0} missing=${s.missing ?? 0} drifted=${s.drifted ?? 0} conflict=${s.conflict ?? 0}`);
|
|
299
|
+
for (const finding of result.findings ?? []) {
|
|
300
|
+
console.log(` ${finding.status.padEnd(10)} ${finding.skillId}${finding.drift ? ` (${finding.drift})` : ""} → ${finding.destinationPath}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** Component Manifest v2 — contract + validation. V1 catalogs normalize in-memory. */
|
|
2
|
+
|
|
3
|
+
import { normalizeIntegrationMetadata } from "./integrations/provider-contract.js";
|
|
4
|
+
|
|
5
|
+
export const COMPONENT_MANIFEST_SCHEMA_VERSION = 2;
|
|
6
|
+
export const COMPONENT_KINDS = Object.freeze(["component"]);
|
|
7
|
+
export const COMPONENT_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
8
|
+
export const COMPONENT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
9
|
+
export const CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9.-]*$/;
|
|
10
|
+
export const HEALTH_CHECK_TYPES = Object.freeze(["assets", "drift", "integration"]);
|
|
11
|
+
|
|
12
|
+
export function normalizeCatalogDocument(document, {
|
|
13
|
+
source = "bundled",
|
|
14
|
+
bundledIds = [],
|
|
15
|
+
requireDefaultEnabled = source === "bundled"
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!isObject(document)) throw new Error("Component catalog must be a JSON object.");
|
|
18
|
+
const schemaVersion = resolveSchemaVersion(document);
|
|
19
|
+
if (!Array.isArray(document.components)) {
|
|
20
|
+
throw new Error("Component catalog must declare a components array.");
|
|
21
|
+
}
|
|
22
|
+
const components = document.components.map((entry, index) =>
|
|
23
|
+
normalizeManifestEntry(entry, { source, requireDefaultEnabled, index })
|
|
24
|
+
);
|
|
25
|
+
validateCatalogGraph(components, { bundledIds, source });
|
|
26
|
+
return { schemaVersion, components };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeManifestEntry(entry, {
|
|
30
|
+
source = "bundled",
|
|
31
|
+
requireDefaultEnabled = false,
|
|
32
|
+
index = null
|
|
33
|
+
} = {}) {
|
|
34
|
+
if (!isObject(entry)) throw new Error(entryError(index, entry?.id, "must be an object."));
|
|
35
|
+
for (const field of ["id", "label", "version", "assetFiles"]) {
|
|
36
|
+
if (entry[field] === undefined) throw new Error(entryError(index, entry.id, `is missing "${field}".`));
|
|
37
|
+
}
|
|
38
|
+
if (requireDefaultEnabled && entry.defaultEnabled === undefined) {
|
|
39
|
+
throw new Error(entryError(index, entry.id, 'is missing "defaultEnabled".'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const id = match(entry.id, COMPONENT_ID_PATTERN)
|
|
43
|
+
? entry.id
|
|
44
|
+
: fail(entryError(index, entry.id, `has invalid id "${entry.id}". Use lowercase letters, digits, and hyphens.`));
|
|
45
|
+
const label = nonEmpty(entry.label, id, "label");
|
|
46
|
+
const version = match(entry.version, COMPONENT_VERSION_PATTERN)
|
|
47
|
+
? entry.version
|
|
48
|
+
: fail(`Component "${id}" has invalid version "${entry.version}". Use semver (e.g. 1.0.0).`);
|
|
49
|
+
const kind = entry.kind === undefined
|
|
50
|
+
? "component"
|
|
51
|
+
: (COMPONENT_KINDS.includes(entry.kind) ? entry.kind : fail(`Component "${id}" has invalid kind "${entry.kind}". Use: ${COMPONENT_KINDS.join(", ")}.`));
|
|
52
|
+
|
|
53
|
+
const normalized = {
|
|
54
|
+
schemaVersion: COMPONENT_MANIFEST_SCHEMA_VERSION,
|
|
55
|
+
id,
|
|
56
|
+
kind,
|
|
57
|
+
label,
|
|
58
|
+
version,
|
|
59
|
+
defaultEnabled: Boolean(requireDefaultEnabled ? entry.defaultEnabled : (entry.defaultEnabled ?? false)),
|
|
60
|
+
capabilities: idList(entry.capabilities, id, "capabilities", CAPABILITY_ID_PATTERN),
|
|
61
|
+
dependencies: idList(entry.dependencies, id, "dependencies", COMPONENT_ID_PATTERN),
|
|
62
|
+
healthChecks: healthChecks(entry.healthChecks, id),
|
|
63
|
+
assetFiles: assets(entry.assetFiles, id),
|
|
64
|
+
adapterHints: hints(entry.adapterHints, id),
|
|
65
|
+
integration: normalizeIntegrationMetadata(entry.integration, { componentId: id, source })
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (source === "workspace" && entry.instructions != null) {
|
|
69
|
+
if (typeof entry.instructions !== "string") fail(`Component "${id}" instructions must be a string.`);
|
|
70
|
+
normalized.instructions = entry.instructions;
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function validateCatalogGraph(components, { bundledIds = [], source = "bundled" } = {}) {
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
const ids = new Set(components.map((c) => c.id));
|
|
78
|
+
for (const component of components) {
|
|
79
|
+
if (seen.has(component.id)) fail(`Duplicate component id "${component.id}".`);
|
|
80
|
+
seen.add(component.id);
|
|
81
|
+
if (source === "workspace" && bundledIds.includes(component.id)) {
|
|
82
|
+
fail(`Workspace component "${component.id}" conflicts with a bundled component.`);
|
|
83
|
+
}
|
|
84
|
+
for (const dependency of component.dependencies) {
|
|
85
|
+
if (dependency === component.id) fail(`Component "${component.id}" cannot depend on itself.`);
|
|
86
|
+
if (!ids.has(dependency) && !bundledIds.includes(dependency)) {
|
|
87
|
+
fail(`Component "${component.id}" depends on unknown component "${dependency}".`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const cycle = detectDependencyCycles(components);
|
|
92
|
+
if (cycle) fail(`Component dependency cycle detected: ${cycle.join(" -> ")}.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function detectDependencyCycles(components) {
|
|
96
|
+
const graph = new Map(components.map((c) => [c.id, c.dependencies]));
|
|
97
|
+
const visiting = new Set();
|
|
98
|
+
const visited = new Set();
|
|
99
|
+
const stack = [];
|
|
100
|
+
for (const id of graph.keys()) {
|
|
101
|
+
const cycle = visit(id);
|
|
102
|
+
if (cycle) return cycle;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
|
|
106
|
+
function visit(id) {
|
|
107
|
+
if (visited.has(id)) return null;
|
|
108
|
+
if (visiting.has(id)) return [...stack.slice(stack.indexOf(id)), id];
|
|
109
|
+
visiting.add(id);
|
|
110
|
+
stack.push(id);
|
|
111
|
+
for (const dependency of graph.get(id) ?? []) {
|
|
112
|
+
if (!graph.has(dependency)) continue;
|
|
113
|
+
const cycle = visit(dependency);
|
|
114
|
+
if (cycle) return cycle;
|
|
115
|
+
}
|
|
116
|
+
stack.pop();
|
|
117
|
+
visiting.delete(id);
|
|
118
|
+
visited.add(id);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function assertSafeAssetPath(assetFile, componentId) {
|
|
124
|
+
if (typeof assetFile !== "string" || !assetFile) fail(`Component "${componentId}" has an invalid asset path.`);
|
|
125
|
+
if (assetFile.startsWith("/") || assetFile.includes("\\") || assetFile.includes("..")) {
|
|
126
|
+
fail(`Component "${componentId}" asset "${assetFile}" must be a relative path without "..".`);
|
|
127
|
+
}
|
|
128
|
+
if (assetFile.endsWith("/")) fail(`Component "${componentId}" asset "${assetFile}" must reference a file.`);
|
|
129
|
+
return assetFile;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveSchemaVersion(document) {
|
|
133
|
+
if (document.schemaVersion === undefined) return 1;
|
|
134
|
+
const version = document.schemaVersion;
|
|
135
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < 1 || version > COMPONENT_MANIFEST_SCHEMA_VERSION) {
|
|
136
|
+
fail(`Unsupported component catalog schemaVersion "${version}".`);
|
|
137
|
+
}
|
|
138
|
+
return version;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assets(assetFiles, id) {
|
|
142
|
+
if (!Array.isArray(assetFiles) || assetFiles.length === 0) fail(`Component "${id}" must declare at least one asset file.`);
|
|
143
|
+
return unique(assetFiles.map((asset) => assertSafeAssetPath(asset, id)), id, "asset");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function idList(value, id, field, pattern) {
|
|
147
|
+
if (value === undefined) return [];
|
|
148
|
+
if (!Array.isArray(value)) fail(`Component "${id}" ${field} must be an array.`);
|
|
149
|
+
return unique(value.map((item) => {
|
|
150
|
+
if (typeof item !== "string" || !pattern.test(item)) fail(`Component "${id}" has invalid ${field} entry "${item}".`);
|
|
151
|
+
return item;
|
|
152
|
+
}), id, field);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function healthChecks(value, id) {
|
|
156
|
+
if (value === undefined) return [];
|
|
157
|
+
if (!Array.isArray(value)) fail(`Component "${id}" healthChecks must be an array.`);
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
return value.map((check) => {
|
|
160
|
+
if (!isObject(check)) fail(`Component "${id}" healthChecks entries must be objects.`);
|
|
161
|
+
if (typeof check.id !== "string" || !CAPABILITY_ID_PATTERN.test(check.id)) {
|
|
162
|
+
fail(`Component "${id}" healthCheck has invalid id "${check.id}".`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof check.type !== "string" || !HEALTH_CHECK_TYPES.includes(check.type)) {
|
|
165
|
+
fail(`Component "${id}" healthCheck "${check.id}" has invalid type "${check.type}".`);
|
|
166
|
+
}
|
|
167
|
+
if (seen.has(check.id)) fail(`Component "${id}" declares duplicate healthCheck "${check.id}".`);
|
|
168
|
+
seen.add(check.id);
|
|
169
|
+
return { id: check.id, type: check.type, optional: Boolean(check.optional) };
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hints(value, id) {
|
|
174
|
+
if (value === undefined) return {};
|
|
175
|
+
if (!isObject(value)) fail(`Component "${id}" adapterHints must be an object.`);
|
|
176
|
+
const normalized = {};
|
|
177
|
+
for (const [key, hint] of Object.entries(value)) {
|
|
178
|
+
if (typeof hint !== "string") fail(`Component "${id}" adapterHints.${key} must be a string.`);
|
|
179
|
+
normalized[key] = hint;
|
|
180
|
+
}
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function unique(items, id, field) {
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const item of items) {
|
|
188
|
+
if (seen.has(item)) fail(`Component "${id}" declares duplicate ${field} entry "${item}".`);
|
|
189
|
+
seen.add(item);
|
|
190
|
+
out.push(item);
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function nonEmpty(value, id, field) {
|
|
196
|
+
if (typeof value !== "string" || !value.trim()) fail(`Component "${id}" ${field} must be a non-empty string.`);
|
|
197
|
+
return value.trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function match(value, pattern) {
|
|
201
|
+
return typeof value === "string" && pattern.test(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isObject(value) {
|
|
205
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function entryError(index, id, message) {
|
|
209
|
+
return `${id ? `Component "${id}"` : `Component catalog entry${index == null ? "" : ` #${index}`}`} ${message}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function fail(message) {
|
|
213
|
+
throw new Error(message);
|
|
214
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { loadComponentCatalog } from "./load-component-catalog.js";
|
|
2
2
|
import { loadWorkspaceComponentCatalog } from "./load-workspace-component-catalog.js";
|
|
3
|
+
import { indexComponentsById, resolveDependencyOrder } from "./component-resolve.js";
|
|
4
|
+
import { ensureIntegrationProvidersRegistered } from "./integrations/index.js";
|
|
5
|
+
|
|
6
|
+
ensureIntegrationProvidersRegistered();
|
|
3
7
|
|
|
4
8
|
const BUNDLED_COMPONENTS = loadComponentCatalog();
|
|
5
9
|
|
|
@@ -24,10 +28,16 @@ function formatCatalogEntry(component) {
|
|
|
24
28
|
id: component.id,
|
|
25
29
|
label: component.label,
|
|
26
30
|
version: component.version,
|
|
31
|
+
schemaVersion: component.schemaVersion ?? 2,
|
|
32
|
+
kind: component.kind ?? "component",
|
|
27
33
|
source: component.source ?? "bundled",
|
|
28
34
|
defaultEnabled: component.defaultEnabled,
|
|
35
|
+
capabilities: [...(component.capabilities ?? [])],
|
|
36
|
+
dependencies: [...(component.dependencies ?? [])],
|
|
37
|
+
healthChecks: (component.healthChecks ?? []).map((check) => ({ ...check })),
|
|
29
38
|
assetFiles: [...component.assetFiles],
|
|
30
39
|
adapterHints: Object.keys(component.adapterHints),
|
|
40
|
+
integration: component.integration ? { ...component.integration } : null,
|
|
31
41
|
instructions: component.instructions ?? null
|
|
32
42
|
};
|
|
33
43
|
}
|
|
@@ -69,12 +79,19 @@ export function resolveTargetComponents({
|
|
|
69
79
|
workspaceRoot = null
|
|
70
80
|
} = {}) {
|
|
71
81
|
if (components != null) {
|
|
72
|
-
return
|
|
82
|
+
return resolveComponentClosure(components, { workspaceRoot });
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
if (noDefaultComponents) return [];
|
|
76
86
|
|
|
77
|
-
return DEFAULT_COMPONENT_IDS
|
|
87
|
+
return resolveComponentClosure(DEFAULT_COMPONENT_IDS, { workspaceRoot });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveComponentClosure(seedIds, { workspaceRoot = null } = {}) {
|
|
91
|
+
const catalog = mergeComponents(workspaceRoot);
|
|
92
|
+
const byId = indexComponentsById(catalog);
|
|
93
|
+
const validatedSeeds = validateComponentIds(seedIds, { workspaceRoot });
|
|
94
|
+
return resolveDependencyOrder(validatedSeeds, byId);
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
export function buildComponentStateEntries(components, adapters) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic component dependency resolution (deps first, no duplicates).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function resolveDependencyOrder(seedIds, componentsById) {
|
|
6
|
+
const seeds = uniqueIds(seedIds);
|
|
7
|
+
const selected = new Set();
|
|
8
|
+
const visiting = new Set();
|
|
9
|
+
const ordered = [];
|
|
10
|
+
|
|
11
|
+
for (const seedId of seeds) {
|
|
12
|
+
visit(seedId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return ordered;
|
|
16
|
+
|
|
17
|
+
function visit(id) {
|
|
18
|
+
if (selected.has(id)) return;
|
|
19
|
+
if (visiting.has(id)) {
|
|
20
|
+
throw new Error(`Component dependency cycle detected while resolving "${id}".`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const component = componentsById.get(id);
|
|
24
|
+
if (!component) {
|
|
25
|
+
throw new Error(`Unknown component "${id}".`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
visiting.add(id);
|
|
29
|
+
for (const dependency of component.dependencies ?? []) {
|
|
30
|
+
visit(dependency);
|
|
31
|
+
}
|
|
32
|
+
visiting.delete(id);
|
|
33
|
+
selected.add(id);
|
|
34
|
+
ordered.push(component);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function indexComponentsById(components) {
|
|
39
|
+
return new Map(components.map((component) => [component.id, component]));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function uniqueIds(ids) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const unique = [];
|
|
45
|
+
for (const id of ids) {
|
|
46
|
+
if (seen.has(id)) continue;
|
|
47
|
+
seen.add(id);
|
|
48
|
+
unique.push(id);
|
|
49
|
+
}
|
|
50
|
+
return unique;
|
|
51
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { SDD_PERSONA_GATE } from "../integrations/sdd-persona.js";
|
|
2
3
|
|
|
3
4
|
export function buildSddCoreManagedSection(context, adapter, catalogEntry) {
|
|
4
5
|
const baseDir = join(context.componentsDir, "sdd-core");
|
|
5
6
|
const adapterNote = catalogEntry?.adapterHints?.[adapter.id] ?? null;
|
|
7
|
+
const teachingPath = join(baseDir, "personas", "teaching.md");
|
|
6
8
|
|
|
7
9
|
return [
|
|
8
10
|
"### SDD Core",
|
|
@@ -10,6 +12,10 @@ export function buildSddCoreManagedSection(context, adapter, catalogEntry) {
|
|
|
10
12
|
`- Workflow: ${join(baseDir, "workflow.md")}`,
|
|
11
13
|
`- Spec sizing: ${join(baseDir, "spec-sizing.md")}`,
|
|
12
14
|
`- Handoff rules: ${join(baseDir, "handoff.md")}`,
|
|
15
|
+
`- Canonical skills: ${join(baseDir, "skills")}`,
|
|
16
|
+
`- Teaching persona: load ${teachingPath} only if adapter id "${adapter.id}" is listed in ${context.paths.statePath} → ${SDD_PERSONA_GATE}.`,
|
|
17
|
+
"- Phase skills: sdd-init, sdd-explore, sdd-propose, sdd-spec, sdd-design, sdd-tasks, sdd-apply, sdd-verify, sdd-archive.",
|
|
18
|
+
"- Teaching persona affects explanations only; never code, docs, commits, or PRs.",
|
|
13
19
|
"- Classify work as basic, standard, or complex before implementing.",
|
|
14
20
|
"- No significant feature without a spec; no bug fix without a failing test first.",
|
|
15
21
|
adapterNote
|