@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.
Files changed (103) hide show
  1. package/README.md +124 -17
  2. package/global-template/components/catalog.json +63 -6
  3. package/global-template/components/sdd-core/personas/teaching.md +42 -0
  4. package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
  5. package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
  6. package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
  7. package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
  8. package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
  9. package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
  10. package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
  11. package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
  12. package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
  13. package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
  14. package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
  15. package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
  16. package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
  17. package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
  18. package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
  19. package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
  20. package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
  21. package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
  22. package/package.json +1 -1
  23. package/scripts/cockpit-smoke.mjs +29 -8
  24. package/scripts/smoke-test.sh +9 -0
  25. package/src/cli.js +54 -5
  26. package/src/global/action-planner.js +17 -4
  27. package/src/global/component-authoring.js +1 -2
  28. package/src/global/component-ecosystem-checks.js +74 -15
  29. package/src/global/component-health.js +56 -0
  30. package/src/global/component-integration-cli.js +302 -0
  31. package/src/global/component-manifest.js +214 -0
  32. package/src/global/component-registry.js +19 -2
  33. package/src/global/component-resolve.js +51 -0
  34. package/src/global/components/sdd-core.js +6 -0
  35. package/src/global/control-plane-proposals.js +229 -0
  36. package/src/global/control-plane-snapshot.js +212 -0
  37. package/src/global/dashboard-guidance.js +27 -3
  38. package/src/global/diff.js +2 -5
  39. package/src/global/global-cli.js +16 -1
  40. package/src/global/global-doctor.js +75 -2
  41. package/src/global/global-installer.js +43 -6
  42. package/src/global/governance-actions.js +212 -0
  43. package/src/global/governance-repair.js +13 -0
  44. package/src/global/ink/cockpit-changes.js +124 -0
  45. package/src/global/ink/cockpit-control-center.js +106 -0
  46. package/src/global/ink/cockpit-controller.js +17 -0
  47. package/src/global/ink/cockpit-enter.js +27 -0
  48. package/src/global/ink/cockpit-focus.js +7 -1
  49. package/src/global/ink/cockpit-models.js +98 -46
  50. package/src/global/ink/cockpit-proposals.js +82 -0
  51. package/src/global/ink/cockpit-recovery.js +120 -0
  52. package/src/global/ink/cockpit-runs.js +47 -0
  53. package/src/global/ink/cockpit-scan.js +60 -0
  54. package/src/global/ink/cockpit-views.js +144 -74
  55. package/src/global/ink/orchestrator-app.js +156 -42
  56. package/src/global/ink/orchestrator-state.js +6 -0
  57. package/src/global/ink/use-orchestrator-data.js +239 -17
  58. package/src/global/integrations/engram-apply.js +105 -0
  59. package/src/global/integrations/engram-evidence.js +218 -0
  60. package/src/global/integrations/engram-exec.js +107 -0
  61. package/src/global/integrations/engram-plan.js +60 -0
  62. package/src/global/integrations/engram-provider.js +22 -0
  63. package/src/global/integrations/engram-receipts.js +116 -0
  64. package/src/global/integrations/engram-rollback.js +131 -0
  65. package/src/global/integrations/index.js +12 -0
  66. package/src/global/integrations/provider-contract.js +83 -0
  67. package/src/global/integrations/provider-registry.js +47 -0
  68. package/src/global/integrations/sdd-apply.js +190 -0
  69. package/src/global/integrations/sdd-destinations.js +95 -0
  70. package/src/global/integrations/sdd-evidence.js +89 -0
  71. package/src/global/integrations/sdd-fs-guard.js +183 -0
  72. package/src/global/integrations/sdd-lifecycle.js +84 -0
  73. package/src/global/integrations/sdd-persona.js +71 -0
  74. package/src/global/integrations/sdd-plan.js +87 -0
  75. package/src/global/integrations/sdd-provider.js +21 -0
  76. package/src/global/integrations/sdd-receipts.js +44 -0
  77. package/src/global/integrations/sdd-rollback.js +269 -0
  78. package/src/global/integrations/sdd-skill-files.js +61 -0
  79. package/src/global/integrations/sdd-state.js +175 -0
  80. package/src/global/integrations/sdd-verify.js +122 -0
  81. package/src/global/intelligence/backends/opencode-catalog.js +212 -0
  82. package/src/global/intelligence/backends/opencode-cli.js +247 -0
  83. package/src/global/intelligence/backends/opencode-evidence.js +129 -0
  84. package/src/global/intelligence/backends/opencode-http.js +152 -0
  85. package/src/global/intelligence/backends/opencode-invoke.js +83 -0
  86. package/src/global/intelligence/backends/opencode-providers.js +67 -0
  87. package/src/global/intelligence/backends/opencode-runtime.js +242 -0
  88. package/src/global/intelligence/context-compiler.js +95 -50
  89. package/src/global/intelligence/index.js +38 -1
  90. package/src/global/intelligence/orchestrate.js +4 -2
  91. package/src/global/intelligence/registry.js +62 -6
  92. package/src/global/intelligence/router.js +188 -70
  93. package/src/global/intelligence/transport-registry.js +147 -0
  94. package/src/global/intelligence/types.js +21 -2
  95. package/src/global/intelligence-cli.js +48 -17
  96. package/src/global/intelligence-session.js +37 -0
  97. package/src/global/json-output.js +1 -0
  98. package/src/global/load-component-catalog.js +10 -28
  99. package/src/global/load-workspace-component-catalog.js +22 -63
  100. package/src/global/state-migration.js +21 -1
  101. package/src/global/state.js +13 -8
  102. package/src/global/status.js +4 -17
  103. 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
- const reload = async () => {
35
- const [dash, diag] = await Promise.all([
36
- buildRuntimeDashboardData({ homeDir, workspaceRoot, cliVersion }),
37
- buildReadOnlyDiagnostics({ homeDir, workspaceRoot, packageName, packageRoot, cliVersion })
38
- ]);
39
- setDashboard(dash);
40
- setDiagnostics(diag);
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.HOME });
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
+ }