@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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { buildReadOnlyDiagnostics } from "../action-planner.js";
|
|
3
|
+
import { buildControlPlaneSnapshot } from "../control-plane-snapshot.js";
|
|
3
4
|
import { buildRuntimeDashboardData } from "../runtime/run-cli.js";
|
|
4
5
|
import { readRunEvents } from "../runtime/run-store.js";
|
|
5
6
|
import { startRun, stopRun } from "../runtime/run-manager.js";
|
|
@@ -10,6 +11,28 @@ import {
|
|
|
10
11
|
resolveLaunchableAgents
|
|
11
12
|
} from "./orchestrator-state.js";
|
|
12
13
|
import { LAUNCH_WIZARD_STEPS, ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
14
|
+
import {
|
|
15
|
+
CONTROL_PLANE_AUTO_SCAN,
|
|
16
|
+
createSerializedReloader,
|
|
17
|
+
loadCockpitScanBundle
|
|
18
|
+
} from "./cockpit-scan.js";
|
|
19
|
+
import {
|
|
20
|
+
CHANGES_PHASE,
|
|
21
|
+
createChangesActionState,
|
|
22
|
+
reduceChangesAction
|
|
23
|
+
} from "./cockpit-changes.js";
|
|
24
|
+
import {
|
|
25
|
+
applyGovernanceSync,
|
|
26
|
+
previewGovernanceSync,
|
|
27
|
+
applyGovernanceRollback,
|
|
28
|
+
previewGovernanceRollback
|
|
29
|
+
} from "../governance-actions.js";
|
|
30
|
+
import {
|
|
31
|
+
RECOVERY_PHASE,
|
|
32
|
+
createRecoveryActionState,
|
|
33
|
+
listRecoverySnapshots,
|
|
34
|
+
reduceRecoveryAction
|
|
35
|
+
} from "./cockpit-recovery.js";
|
|
13
36
|
|
|
14
37
|
export function useOrchestratorData({
|
|
15
38
|
homeDir,
|
|
@@ -20,9 +43,11 @@ export function useOrchestratorData({
|
|
|
20
43
|
}) {
|
|
21
44
|
const [loading, setLoading] = useState(true);
|
|
22
45
|
const [busy, setBusy] = useState(false);
|
|
46
|
+
const [retrying, setRetrying] = useState(false);
|
|
23
47
|
const [error, setError] = useState(null);
|
|
24
48
|
const [dashboard, setDashboard] = useState(null);
|
|
25
49
|
const [diagnostics, setDiagnostics] = useState(null);
|
|
50
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
26
51
|
const [selectedRun, setSelectedRun] = useState(null);
|
|
27
52
|
const [selectedEvents, setSelectedEvents] = useState([]);
|
|
28
53
|
const [statusMessage, setStatusMessage] = useState(null);
|
|
@@ -30,27 +55,50 @@ export function useOrchestratorData({
|
|
|
30
55
|
const [launchStep, setLaunchStep] = useState(LAUNCH_WIZARD_STEPS.AGENT);
|
|
31
56
|
const [launchDraft, setLaunchDraft] = useState(createLaunchDraft);
|
|
32
57
|
const [launchPermissionIndex, setLaunchPermissionIndex] = useState(0);
|
|
58
|
+
const [changesAction, setChangesAction] = useState(createChangesActionState);
|
|
59
|
+
const [recoveryAction, setRecoveryAction] = useState(createRecoveryActionState);
|
|
60
|
+
|
|
61
|
+
const serializedReload = useMemo(() => createSerializedReloader(() => loadCockpitScanBundle({
|
|
62
|
+
homeDir,
|
|
63
|
+
workspaceRoot,
|
|
64
|
+
packageName,
|
|
65
|
+
packageRoot,
|
|
66
|
+
cliVersion,
|
|
67
|
+
buildDashboard: buildRuntimeDashboardData,
|
|
68
|
+
buildDiagnostics: buildReadOnlyDiagnostics,
|
|
69
|
+
buildSnapshot: buildControlPlaneSnapshot
|
|
70
|
+
})), [homeDir, workspaceRoot, packageName, packageRoot, cliVersion]);
|
|
71
|
+
|
|
72
|
+
const reload = async ({ showLoading = false, asRetry = false } = {}) => {
|
|
73
|
+
if (showLoading) setLoading(true);
|
|
74
|
+
if (asRetry) setRetrying(true);
|
|
75
|
+
const outcome = await serializedReload();
|
|
76
|
+
// Keep retrying=true across stale completions so overlapping retries don't clear too early.
|
|
77
|
+
if (outcome.stale) return outcome;
|
|
78
|
+
|
|
79
|
+
if (outcome.error) {
|
|
80
|
+
setError(outcome.error instanceof Error ? outcome.error.message : String(outcome.error));
|
|
81
|
+
setLoading(false);
|
|
82
|
+
setRetrying(false);
|
|
83
|
+
throw outcome.error;
|
|
84
|
+
}
|
|
33
85
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
setDashboard(outcome.result.dashboard);
|
|
87
|
+
setDiagnostics(outcome.result.diagnostics);
|
|
88
|
+
setSnapshot(outcome.result.snapshot);
|
|
89
|
+
setError(null);
|
|
90
|
+
setLoading(false);
|
|
91
|
+
setRetrying(false);
|
|
92
|
+
return outcome;
|
|
41
93
|
};
|
|
42
94
|
|
|
43
95
|
useEffect(() => {
|
|
44
96
|
let cancelled = false;
|
|
45
97
|
async function load() {
|
|
46
98
|
try {
|
|
47
|
-
await reload();
|
|
99
|
+
await reload({ showLoading: true });
|
|
100
|
+
} catch {
|
|
48
101
|
if (!cancelled) setLoading(false);
|
|
49
|
-
} catch (loadError) {
|
|
50
|
-
if (!cancelled) {
|
|
51
|
-
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
|
52
|
-
setLoading(false);
|
|
53
|
-
}
|
|
54
102
|
}
|
|
55
103
|
}
|
|
56
104
|
load();
|
|
@@ -105,8 +153,8 @@ export function useOrchestratorData({
|
|
|
105
153
|
.runs.find((entry) => entry.runId === runId);
|
|
106
154
|
setStatusMessage(`Run started: ${runId}`);
|
|
107
155
|
resetLaunchWizard();
|
|
108
|
-
dispatch({ type: "set-view", view: ORCHESTRATOR_VIEWS.
|
|
109
|
-
if (run) await openRunDetail(run, dispatch);
|
|
156
|
+
dispatch({ type: "set-view", view: ORCHESTRATOR_VIEWS.RUNS });
|
|
157
|
+
if (run) await openRunDetail(run, dispatch, ORCHESTRATOR_VIEWS.ACTIVE_RUNS);
|
|
110
158
|
} catch (launchError) {
|
|
111
159
|
setError(launchError instanceof Error ? launchError.message : String(launchError));
|
|
112
160
|
} finally {
|
|
@@ -133,13 +181,175 @@ export function useOrchestratorData({
|
|
|
133
181
|
}
|
|
134
182
|
};
|
|
135
183
|
|
|
184
|
+
const previewChanges = async () => {
|
|
185
|
+
setChangesAction((prev) => reduceChangesAction(prev, { type: "preview-start" }));
|
|
186
|
+
setBusy(true);
|
|
187
|
+
try {
|
|
188
|
+
const preview = await previewGovernanceSync({
|
|
189
|
+
homeDir, workspaceRoot, packageName, packageRoot, cliVersion
|
|
190
|
+
});
|
|
191
|
+
setChangesAction((prev) => reduceChangesAction(prev, { type: "preview-ready", preview }));
|
|
192
|
+
return preview;
|
|
193
|
+
} catch (previewError) {
|
|
194
|
+
const message = previewError instanceof Error ? previewError.message : String(previewError);
|
|
195
|
+
setChangesAction((prev) => reduceChangesAction(prev, {
|
|
196
|
+
type: "preview-failed", error: "preview-failed", message
|
|
197
|
+
}));
|
|
198
|
+
return null;
|
|
199
|
+
} finally {
|
|
200
|
+
setBusy(false);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const cancelChanges = () => {
|
|
205
|
+
setChangesAction((prev) => reduceChangesAction(prev, { type: "cancel" }));
|
|
206
|
+
setStatusMessage("Cancelled — no files written.");
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const confirmApplyChanges = async () => {
|
|
210
|
+
let preview = null;
|
|
211
|
+
setChangesAction((prev) => {
|
|
212
|
+
if (prev.phase !== CHANGES_PHASE.CONFIRMING || !prev.preview) return prev;
|
|
213
|
+
preview = prev.preview;
|
|
214
|
+
return reduceChangesAction(prev, { type: "apply-start" });
|
|
215
|
+
});
|
|
216
|
+
if (!preview) return null;
|
|
217
|
+
setBusy(true);
|
|
218
|
+
try {
|
|
219
|
+
const result = await applyGovernanceSync({
|
|
220
|
+
preview,
|
|
221
|
+
homeDir, workspaceRoot, packageName, packageRoot, cliVersion
|
|
222
|
+
});
|
|
223
|
+
if (result.reason === "stale-preview") {
|
|
224
|
+
setChangesAction((prev) => reduceChangesAction(prev, {
|
|
225
|
+
type: "apply-done",
|
|
226
|
+
ok: false,
|
|
227
|
+
reason: "stale-preview",
|
|
228
|
+
preview: result.preview,
|
|
229
|
+
message: "Preview stale — press A for a fresh preview."
|
|
230
|
+
}));
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
if (result.reason === "setup-required") {
|
|
234
|
+
setChangesAction((prev) => reduceChangesAction(prev, {
|
|
235
|
+
type: "preview-ready",
|
|
236
|
+
preview: { setupRequired: true }
|
|
237
|
+
}));
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
await reload();
|
|
241
|
+
setChangesAction((prev) => reduceChangesAction(prev, {
|
|
242
|
+
type: "apply-done",
|
|
243
|
+
ok: result.ok,
|
|
244
|
+
reason: result.reason,
|
|
245
|
+
receipt: result.receipt,
|
|
246
|
+
message: result.ok
|
|
247
|
+
? (result.partial ? "Applied with partial evidence — re-scan complete." : "Applied — re-scan complete.")
|
|
248
|
+
: `Apply ${result.reason}.`
|
|
249
|
+
}));
|
|
250
|
+
setStatusMessage(result.ok ? "Governance apply recorded." : null);
|
|
251
|
+
return result;
|
|
252
|
+
} catch (applyError) {
|
|
253
|
+
const message = applyError instanceof Error ? applyError.message : String(applyError);
|
|
254
|
+
setChangesAction((prev) => reduceChangesAction(prev, {
|
|
255
|
+
type: "apply-done", ok: false, reason: "apply-failed", message
|
|
256
|
+
}));
|
|
257
|
+
return null;
|
|
258
|
+
} finally {
|
|
259
|
+
setBusy(false);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const rescanChanges = async () => {
|
|
264
|
+
setChangesAction(() => createChangesActionState());
|
|
265
|
+
await reload();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const previewRecovery = async (snapshotName) => {
|
|
269
|
+
if (!snapshotName) return null;
|
|
270
|
+
setRecoveryAction((prev) => reduceRecoveryAction(prev, {
|
|
271
|
+
type: "preview-start", snapshot: snapshotName
|
|
272
|
+
}));
|
|
273
|
+
setBusy(true);
|
|
274
|
+
try {
|
|
275
|
+
const preview = await previewGovernanceRollback({ homeDir, snapshot: snapshotName });
|
|
276
|
+
setRecoveryAction((prev) => reduceRecoveryAction(prev, { type: "preview-ready", preview }));
|
|
277
|
+
return preview;
|
|
278
|
+
} catch (previewError) {
|
|
279
|
+
const message = previewError instanceof Error ? previewError.message : String(previewError);
|
|
280
|
+
setRecoveryAction((prev) => reduceRecoveryAction(prev, {
|
|
281
|
+
type: "preview-failed", error: "preview-failed", message
|
|
282
|
+
}));
|
|
283
|
+
return null;
|
|
284
|
+
} finally {
|
|
285
|
+
setBusy(false);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const cancelRecovery = () => {
|
|
290
|
+
setRecoveryAction((prev) => reduceRecoveryAction(prev, { type: "cancel" }));
|
|
291
|
+
setStatusMessage("Cancelled — previous snapshot kept.");
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const confirmApplyRecovery = async () => {
|
|
295
|
+
let preview = null;
|
|
296
|
+
setRecoveryAction((prev) => {
|
|
297
|
+
if (prev.phase !== RECOVERY_PHASE.CONFIRMING || !prev.preview) return prev;
|
|
298
|
+
preview = prev.preview;
|
|
299
|
+
return reduceRecoveryAction(prev, { type: "apply-start" });
|
|
300
|
+
});
|
|
301
|
+
if (!preview) return null;
|
|
302
|
+
setBusy(true);
|
|
303
|
+
try {
|
|
304
|
+
const result = await applyGovernanceRollback({
|
|
305
|
+
preview, homeDir, cliVersion
|
|
306
|
+
});
|
|
307
|
+
if (result.reason === "stale-preview") {
|
|
308
|
+
setRecoveryAction((prev) => reduceRecoveryAction(prev, {
|
|
309
|
+
type: "apply-done",
|
|
310
|
+
ok: false,
|
|
311
|
+
reason: "stale-preview",
|
|
312
|
+
preview: result.preview,
|
|
313
|
+
message: "Preview stale — press Enter for a fresh preview."
|
|
314
|
+
}));
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
await reload();
|
|
318
|
+
setRecoveryAction((prev) => reduceRecoveryAction(prev, {
|
|
319
|
+
type: "apply-done",
|
|
320
|
+
ok: result.ok,
|
|
321
|
+
reason: result.reason,
|
|
322
|
+
receipt: result.receipt,
|
|
323
|
+
message: result.ok ? "Rollback applied — re-scan complete." : `Rollback ${result.reason}.`
|
|
324
|
+
}));
|
|
325
|
+
setStatusMessage(result.ok ? "Rollback recorded." : null);
|
|
326
|
+
return result;
|
|
327
|
+
} catch (applyError) {
|
|
328
|
+
const message = applyError instanceof Error ? applyError.message : String(applyError);
|
|
329
|
+
// Keep prior preview so the selected snapshot remains visible after failure.
|
|
330
|
+
setRecoveryAction((prev) => reduceRecoveryAction(prev, {
|
|
331
|
+
type: "apply-done", ok: false, reason: "apply-failed", message, preview: prev.preview
|
|
332
|
+
}));
|
|
333
|
+
return null;
|
|
334
|
+
} finally {
|
|
335
|
+
setBusy(false);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const rescanRecovery = async () => {
|
|
340
|
+
setRecoveryAction(() => createRecoveryActionState());
|
|
341
|
+
await reload();
|
|
342
|
+
};
|
|
343
|
+
|
|
136
344
|
return {
|
|
137
345
|
loading,
|
|
138
346
|
busy,
|
|
347
|
+
retrying,
|
|
139
348
|
error,
|
|
140
349
|
setError,
|
|
141
350
|
dashboard,
|
|
142
351
|
diagnostics,
|
|
352
|
+
snapshot,
|
|
143
353
|
selectedRun,
|
|
144
354
|
setSelectedRun,
|
|
145
355
|
selectedEvents,
|
|
@@ -153,10 +363,22 @@ export function useOrchestratorData({
|
|
|
153
363
|
launchPermissionIndex,
|
|
154
364
|
setLaunchPermissionIndex,
|
|
155
365
|
launchableAgents: resolveLaunchableAgents(dashboard?.providers ?? []),
|
|
366
|
+
scanOptions: CONTROL_PLANE_AUTO_SCAN,
|
|
367
|
+
changesAction,
|
|
368
|
+
recoveryAction,
|
|
156
369
|
reload,
|
|
157
370
|
resetLaunchWizard,
|
|
158
371
|
openRunDetail,
|
|
159
372
|
handleLaunch,
|
|
160
|
-
handleCancelRun
|
|
373
|
+
handleCancelRun,
|
|
374
|
+
previewChanges,
|
|
375
|
+
cancelChanges,
|
|
376
|
+
confirmApplyChanges,
|
|
377
|
+
rescanChanges,
|
|
378
|
+
previewRecovery,
|
|
379
|
+
cancelRecovery,
|
|
380
|
+
confirmApplyRecovery,
|
|
381
|
+
rescanRecovery,
|
|
382
|
+
recoverySnapshots: listRecoverySnapshots(snapshot)
|
|
161
383
|
};
|
|
162
384
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertExplicitApplyConsent,
|
|
3
|
+
promptApplyConfirmation,
|
|
4
|
+
shouldPromptApplyConfirmation
|
|
5
|
+
} from "../apply-confirmation.js";
|
|
6
|
+
import { ENGRAM_INTEGRATION_STATUS } from "./engram-evidence.js";
|
|
7
|
+
import { planEngramConfigure } from "./engram-plan.js";
|
|
8
|
+
import { runEngramSetup } from "./engram-exec.js";
|
|
9
|
+
|
|
10
|
+
/** Apply Engram configure with consent; stop on first failure with partial receipt. */
|
|
11
|
+
export async function applyEngramConfigure({
|
|
12
|
+
requestedAgentIds = null,
|
|
13
|
+
detectedAgentIds = [],
|
|
14
|
+
env = process.env,
|
|
15
|
+
homeDir,
|
|
16
|
+
dryRun = false,
|
|
17
|
+
yes = false,
|
|
18
|
+
json = false,
|
|
19
|
+
interactive = null,
|
|
20
|
+
receiptId = null,
|
|
21
|
+
plan = planEngramConfigure,
|
|
22
|
+
runSetup = runEngramSetup,
|
|
23
|
+
confirm = promptApplyConfirmation,
|
|
24
|
+
now = () => new Date().toISOString()
|
|
25
|
+
} = {}) {
|
|
26
|
+
assertExplicitApplyConsent({
|
|
27
|
+
applying: !dryRun, dryRun, json, yes, interactive, command: "components configure"
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const planned = plan({ requestedAgentIds, detectedAgentIds, env, homeDir, dryRun: true });
|
|
31
|
+
if (dryRun) return { ...planned, applied: false, cancelled: false, receipt: null };
|
|
32
|
+
if (planned.blocked) {
|
|
33
|
+
throw new Error(planned.guidance ?? "Engram configure is blocked until the binary is supported.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (shouldPromptApplyConfirmation({ applying: true, dryRun, json, confirm: yes, interactive })) {
|
|
37
|
+
const accepted = await confirm({
|
|
38
|
+
command: "components configure engram-memory",
|
|
39
|
+
question: "Apply Engram setup for the planned agents? [Y/n]: "
|
|
40
|
+
});
|
|
41
|
+
if (!accepted) return { ...planned, applied: false, cancelled: true, receipt: null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startedAt = now();
|
|
45
|
+
const agentResults = [];
|
|
46
|
+
let failed = null;
|
|
47
|
+
|
|
48
|
+
for (const action of planned.actions) {
|
|
49
|
+
if (action.action === "blocked" || !action.command) {
|
|
50
|
+
failed = { agentId: action.agentId, slug: action.slug, error: action.reason ?? "Missing setup command." };
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const [binaryPath, , slug] = action.command;
|
|
54
|
+
const result = await runSetup({ binaryPath, slug, env });
|
|
55
|
+
agentResults.push({
|
|
56
|
+
agentId: action.agentId, slug, ok: result.ok, status: result.status,
|
|
57
|
+
timedOut: result.timedOut, terminationFailed: result.terminationFailed,
|
|
58
|
+
command: result.command, stdout: result.stdout, stderr: result.stderr
|
|
59
|
+
});
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
failed = {
|
|
62
|
+
agentId: action.agentId,
|
|
63
|
+
slug,
|
|
64
|
+
error: result.timedOut
|
|
65
|
+
? `Engram setup timed out for ${slug}.`
|
|
66
|
+
: `Engram setup failed for ${slug} (exit ${result.status}).`
|
|
67
|
+
};
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const completed = planned.actions
|
|
73
|
+
.filter((action) => agentResults.some((entry) => entry.agentId === action.agentId && entry.ok))
|
|
74
|
+
.map((action) => action.agentId);
|
|
75
|
+
const remaining = planned.actions.map((a) => a.agentId).filter((id) => !completed.includes(id));
|
|
76
|
+
|
|
77
|
+
const receipt = {
|
|
78
|
+
id: receiptId ?? `engram-${startedAt.replace(/[:.]/g, "-")}`,
|
|
79
|
+
provider: "engram",
|
|
80
|
+
componentId: "engram-memory",
|
|
81
|
+
startedAt,
|
|
82
|
+
finishedAt: now(),
|
|
83
|
+
binary: planned.binary,
|
|
84
|
+
agentsRequested: planned.actions.map((a) => a.agentId),
|
|
85
|
+
agentsCompleted: completed,
|
|
86
|
+
agentsRemaining: remaining,
|
|
87
|
+
agentResults,
|
|
88
|
+
ok: failed == null,
|
|
89
|
+
partial: failed != null && completed.length > 0,
|
|
90
|
+
failed,
|
|
91
|
+
status: failed == null ? ENGRAM_INTEGRATION_STATUS.RESTART_REQUIRED : ENGRAM_INTEGRATION_STATUS.CONFLICT,
|
|
92
|
+
persisted: false,
|
|
93
|
+
touchedMemoryDb: false
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...planned,
|
|
98
|
+
dryRun: false,
|
|
99
|
+
executes: true,
|
|
100
|
+
writes: true,
|
|
101
|
+
applied: failed == null,
|
|
102
|
+
cancelled: false,
|
|
103
|
+
receipt
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { probeCommand } from "../cli-probe.js";
|
|
5
|
+
|
|
6
|
+
export const KAIRO_ENGRAM_AGENT_IDS = Object.freeze(["cursor", "codex", "opencode", "claude"]);
|
|
7
|
+
export const ENGRAM_SETUP_SLUG_BY_AGENT = Object.freeze({
|
|
8
|
+
cursor: "cursor",
|
|
9
|
+
codex: "codex",
|
|
10
|
+
opencode: "opencode",
|
|
11
|
+
claude: "claude-code"
|
|
12
|
+
});
|
|
13
|
+
export const ENGRAM_MIN_VERSION = "1.19.0";
|
|
14
|
+
export const ENGRAM_MAX_MAJOR = 2;
|
|
15
|
+
export const ENGRAM_INTEGRATION_STATUS = Object.freeze({
|
|
16
|
+
MISSING: "missing",
|
|
17
|
+
UNSUPPORTED: "unsupported",
|
|
18
|
+
AVAILABLE: "available",
|
|
19
|
+
UNCONFIGURED: "unconfigured",
|
|
20
|
+
CONFIGURED: "configured",
|
|
21
|
+
CONFLICT: "conflict",
|
|
22
|
+
RESTART_REQUIRED: "restart_required"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function engramSetupSlugForAgent(agentId) {
|
|
26
|
+
const slug = ENGRAM_SETUP_SLUG_BY_AGENT[agentId];
|
|
27
|
+
if (!slug) {
|
|
28
|
+
throw new Error(`Agent "${agentId}" is not managed for Engram setup. Use: ${KAIRO_ENGRAM_AGENT_IDS.join(", ")}.`);
|
|
29
|
+
}
|
|
30
|
+
return slug;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveEngramAgentSelection({
|
|
34
|
+
requestedIds = null,
|
|
35
|
+
detectedIds = [],
|
|
36
|
+
managedIds = KAIRO_ENGRAM_AGENT_IDS
|
|
37
|
+
} = {}) {
|
|
38
|
+
const managed = [...managedIds];
|
|
39
|
+
if (requestedIds == null) {
|
|
40
|
+
const detected = new Set(detectedIds);
|
|
41
|
+
return managed.filter((id) => detected.has(id));
|
|
42
|
+
}
|
|
43
|
+
if (requestedIds.length === 1 && requestedIds[0] === "all") return [...managed];
|
|
44
|
+
return requestedIds.map((id) => {
|
|
45
|
+
if (!managed.includes(id)) {
|
|
46
|
+
throw new Error(`Agent "${id}" is not managed for Engram setup. Use: ${managed.join(", ")}.`);
|
|
47
|
+
}
|
|
48
|
+
return id;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseEngramVersion(output) {
|
|
53
|
+
const text = String(output ?? "");
|
|
54
|
+
const labeled = text.match(/\bengram\s+v?(\d+\.\d+\.\d+)\b/i);
|
|
55
|
+
if (labeled) return labeled[1];
|
|
56
|
+
for (const line of text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)) {
|
|
57
|
+
if (/update available/i.test(line)) continue;
|
|
58
|
+
const match = line.match(/^v?(\d+\.\d+\.\d+)\b/);
|
|
59
|
+
if (match) return match[1];
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function classifyEngramVersion(version) {
|
|
65
|
+
const parsed = parseSemver(version);
|
|
66
|
+
const range = `>=${ENGRAM_MIN_VERSION} <${ENGRAM_MAX_MAJOR}.0.0`;
|
|
67
|
+
if (!parsed) {
|
|
68
|
+
return unsupported(`Unable to parse Engram version "${version}". Supported range: ${range}.`);
|
|
69
|
+
}
|
|
70
|
+
if (parsed.major >= ENGRAM_MAX_MAJOR || parsed.major < 1) {
|
|
71
|
+
return unsupported(`Engram ${version} is outside the supported major range (${range}).`);
|
|
72
|
+
}
|
|
73
|
+
if (compareSemver(parsed, parseSemver(ENGRAM_MIN_VERSION)) < 0) {
|
|
74
|
+
return unsupported(
|
|
75
|
+
`Engram ${version} is below the supported baseline ${ENGRAM_MIN_VERSION}. Upgrade Engram manually (Kairo will not update it).`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return { status: ENGRAM_INTEGRATION_STATUS.AVAILABLE, supported: true, guidance: null };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function inspectEngramBinary({ env = process.env, probe = probeCommand, whichCommand = defaultWhich } = {}) {
|
|
82
|
+
const path = whichCommand("engram", env) || null;
|
|
83
|
+
if (!path) {
|
|
84
|
+
return {
|
|
85
|
+
path: null,
|
|
86
|
+
version: null,
|
|
87
|
+
status: ENGRAM_INTEGRATION_STATUS.MISSING,
|
|
88
|
+
supported: false,
|
|
89
|
+
guidance: "Engram binary not found in PATH. Install Engram separately, then re-run configure."
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const result = probe(path, ["version"], { env, timeoutMs: 5000 });
|
|
93
|
+
const version = parseEngramVersion(`${result.stdout}\n${result.stderr}`);
|
|
94
|
+
if (!version) {
|
|
95
|
+
return {
|
|
96
|
+
path,
|
|
97
|
+
version: null,
|
|
98
|
+
status: ENGRAM_INTEGRATION_STATUS.UNSUPPORTED,
|
|
99
|
+
supported: false,
|
|
100
|
+
guidance: "Engram binary found but `engram version` did not return a parseable version."
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const classified = classifyEngramVersion(version);
|
|
104
|
+
return { path, version, status: classified.status, supported: classified.supported, guidance: classified.guidance };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function inspectEngramAgentConfig(agentId, { homeDir = homedir() } = {}) {
|
|
108
|
+
const slug = engramSetupSlugForAgent(agentId);
|
|
109
|
+
const evidence = collectAgentEvidence(agentId, homeDir);
|
|
110
|
+
const conflicts = evidence.filter((item) => item.conflict);
|
|
111
|
+
let status = ENGRAM_INTEGRATION_STATUS.UNCONFIGURED;
|
|
112
|
+
if (conflicts.length > 0) status = ENGRAM_INTEGRATION_STATUS.CONFLICT;
|
|
113
|
+
else if (evidence.some((item) => item.present && item.kind === "mcp")) {
|
|
114
|
+
status = ENGRAM_INTEGRATION_STATUS.CONFIGURED;
|
|
115
|
+
}
|
|
116
|
+
return { id: agentId, slug, status, evidence };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function inspectEngramIntegration({
|
|
120
|
+
env = process.env,
|
|
121
|
+
homeDir = homedir(),
|
|
122
|
+
agentIds = KAIRO_ENGRAM_AGENT_IDS,
|
|
123
|
+
probe = probeCommand,
|
|
124
|
+
whichCommand = defaultWhich
|
|
125
|
+
} = {}) {
|
|
126
|
+
const binary = inspectEngramBinary({ env, probe, whichCommand });
|
|
127
|
+
const agents = agentIds.map((id) => inspectEngramAgentConfig(id, { homeDir }));
|
|
128
|
+
let status = binary.status;
|
|
129
|
+
if (binary.supported) {
|
|
130
|
+
if (agents.some((a) => a.status === ENGRAM_INTEGRATION_STATUS.CONFLICT)) {
|
|
131
|
+
status = ENGRAM_INTEGRATION_STATUS.CONFLICT;
|
|
132
|
+
} else if (agents.every((a) => a.status === ENGRAM_INTEGRATION_STATUS.CONFIGURED)) {
|
|
133
|
+
status = ENGRAM_INTEGRATION_STATUS.CONFIGURED;
|
|
134
|
+
} else if (agents.some((a) => a.status === ENGRAM_INTEGRATION_STATUS.CONFIGURED)) {
|
|
135
|
+
status = ENGRAM_INTEGRATION_STATUS.AVAILABLE;
|
|
136
|
+
} else {
|
|
137
|
+
status = ENGRAM_INTEGRATION_STATUS.UNCONFIGURED;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { provider: "engram", status, binary, agents, doctorInvoked: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function collectAgentEvidence(agentId, homeDir) {
|
|
144
|
+
if (agentId === "claude") {
|
|
145
|
+
return [
|
|
146
|
+
fileEvidence(join(homeDir, ".claude", "mcp", "engram.json"), "mcp"),
|
|
147
|
+
jsonKeyEvidence(join(homeDir, ".claude", "settings.json"), ["mcpServers", "engram"], "mcp")
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
if (agentId === "codex") {
|
|
151
|
+
return [
|
|
152
|
+
tomlSectionEvidence(join(homeDir, ".codex", "config.toml"), "mcp_servers.engram", "mcp"),
|
|
153
|
+
fileEvidence(join(homeDir, ".codex", "engram-instructions.md"), "protocol")
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
if (agentId === "opencode") {
|
|
157
|
+
return [
|
|
158
|
+
jsonKeyEvidence(join(homeDir, ".config", "opencode", "opencode.json"), ["mcp", "engram"], "mcp"),
|
|
159
|
+
fileEvidence(join(homeDir, ".config", "opencode", "plugins", "engram.ts"), "plugin")
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
if (agentId === "cursor") {
|
|
163
|
+
return [
|
|
164
|
+
jsonKeyEvidence(join(homeDir, ".cursor", "mcp.json"), ["mcpServers", "engram"], "mcp"),
|
|
165
|
+
fileEvidence(join(homeDir, ".cursor", "rules", "engram.mdc"), "protocol")
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Unsupported Engram agent "${agentId}".`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function fileEvidence(path, kind) {
|
|
172
|
+
return { path, kind, present: existsSync(path), conflict: false };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function jsonKeyEvidence(path, keyPath, kind) {
|
|
176
|
+
if (!existsSync(path)) return { path, kind, present: false, conflict: false, keyPath };
|
|
177
|
+
try {
|
|
178
|
+
let cursor = JSON.parse(readFileSync(path, "utf8"));
|
|
179
|
+
for (const key of keyPath) {
|
|
180
|
+
if (cursor == null || typeof cursor !== "object") {
|
|
181
|
+
return { path, kind, present: false, conflict: true, keyPath, detail: "invalid structure" };
|
|
182
|
+
}
|
|
183
|
+
cursor = cursor[key];
|
|
184
|
+
}
|
|
185
|
+
return { path, kind, present: cursor != null && typeof cursor === "object", conflict: false, keyPath };
|
|
186
|
+
} catch {
|
|
187
|
+
return { path, kind, present: false, conflict: true, keyPath, detail: "unreadable json" };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function tomlSectionEvidence(path, section, kind) {
|
|
192
|
+
if (!existsSync(path)) return { path, kind, present: false, conflict: false, section };
|
|
193
|
+
try {
|
|
194
|
+
const text = readFileSync(path, "utf8");
|
|
195
|
+
const present = new RegExp(`\\[\\s*${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\]`).test(text);
|
|
196
|
+
return { path, kind, present, conflict: false, section };
|
|
197
|
+
} catch {
|
|
198
|
+
return { path, kind, present: false, conflict: true, section, detail: "unreadable toml" };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function unsupported(guidance) {
|
|
203
|
+
return { status: ENGRAM_INTEGRATION_STATUS.UNSUPPORTED, supported: false, guidance };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseSemver(version) {
|
|
207
|
+
const match = String(version ?? "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
208
|
+
return match ? { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) } : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function compareSemver(a, b) {
|
|
212
|
+
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function defaultWhich(command, env) {
|
|
216
|
+
const result = probeCommand("which", [command], { env, timeoutMs: 3000 });
|
|
217
|
+
return result.ok ? result.stdout.trim() : null;
|
|
218
|
+
}
|