@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
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Deterministic evidence-backed control-plane proposals.
3
+ * Optional intelligence absence never yields a proposal.
4
+ * Health strings match CONTROL_PLANE_HEALTH in control-plane-snapshot.js.
5
+ */
6
+
7
+ export const PROPOSAL_SEVERITY = Object.freeze({
8
+ HIGH: "high",
9
+ MEDIUM: "medium",
10
+ LOW: "low",
11
+ INFO: "info"
12
+ });
13
+
14
+ export const PROPOSAL_DESTINATION = Object.freeze({
15
+ CHANGES: "changes",
16
+ CONTROL_CENTER: "control-center",
17
+ IDES: "ides",
18
+ MODULES: "modules",
19
+ ACTIVITY: "activity",
20
+ PROFILE: "profile"
21
+ });
22
+
23
+ const SEVERITY_RANK = Object.freeze({
24
+ high: 0,
25
+ medium: 1,
26
+ low: 2,
27
+ info: 3
28
+ });
29
+
30
+ const ACTIONABLE = new Set(["missing", "stale", "warning", "failed"]);
31
+
32
+ /**
33
+ * Derive proposals only from status, checks, diff, adapters, and policy.
34
+ * No proposal without evidence; deduped by id; severity then id order.
35
+ */
36
+ export function buildControlPlaneProposals({
37
+ health = null,
38
+ status = null,
39
+ adapters = null,
40
+ policy = null,
41
+ diff = null
42
+ } = {}) {
43
+ const out = [];
44
+
45
+ if (health === "NOT_CONFIGURED" || status?.overall === "missing") {
46
+ out.push(make(
47
+ "setup-local",
48
+ PROPOSAL_SEVERITY.HIGH,
49
+ "Finish local setup",
50
+ status?.nextAction ?? "Configure the local ecosystem before applying repairs.",
51
+ PROPOSAL_DESTINATION.CHANGES,
52
+ [
53
+ ev("status", "status.overall", status?.overall ?? "missing"),
54
+ ev("health", "control-plane.health", health ?? "NOT_CONFIGURED")
55
+ ]
56
+ ));
57
+ }
58
+
59
+ const hasDiff = Boolean(diff?.hasChanges);
60
+ const isDrift = status?.overall === "drift" || health === "ACTION_REQUIRED";
61
+ // Gate on health/overall drift only. diff.hasChanges can be true from
62
+ // needsManagedRepair SDD integration warnings while overall stays "ok".
63
+ if (isDrift) {
64
+ const evidence = [
65
+ ...(diff?.changes ?? []).slice(0, 8).map((change) =>
66
+ ev("diff", "diff.change", change.target ?? change.path ?? change.action ?? "managed-change")),
67
+ ...(status?.checks ?? [])
68
+ .filter((check) => (check.status === "stale" || check.status === "missing") && !isIntel(check))
69
+ .slice(0, 8)
70
+ .map((check) => ev("check", `status.checks.${check.name}`, check.status)),
71
+ ev("status", "status.overall", status?.overall ?? "drift")
72
+ ];
73
+ out.push(make(
74
+ "repair-drift",
75
+ PROPOSAL_SEVERITY.HIGH,
76
+ "Review and repair managed drift",
77
+ status?.nextAction
78
+ ?? (hasDiff
79
+ ? `${diff.changeCount ?? diff.changes?.length ?? 0} managed change(s) pending preview.`
80
+ : "Managed content has drifted from the expected inventory."),
81
+ PROPOSAL_DESTINATION.CHANGES,
82
+ evidence
83
+ ));
84
+ }
85
+
86
+ if (health === "CHECK_FAILED" || status?.overall === "failed") {
87
+ const failed = (status?.checks ?? [])
88
+ .filter((check) => (check.status === "failed" || check.status === "missing") && !isIntel(check));
89
+ if (failed.length === 0) {
90
+ out.push(make(
91
+ "verify-failed-overall",
92
+ PROPOSAL_SEVERITY.HIGH,
93
+ "Investigate failed governance checks",
94
+ status?.nextAction ?? "Doctor reported a failed overall status.",
95
+ PROPOSAL_DESTINATION.CONTROL_CENTER,
96
+ [ev("status", "status.overall", status?.overall ?? "failed")]
97
+ ));
98
+ } else {
99
+ for (const check of failed) {
100
+ out.push(make(
101
+ `verify-check-${slug(check.name)}`,
102
+ PROPOSAL_SEVERITY.HIGH,
103
+ `Investigate check: ${check.name}`,
104
+ check.detail ?? `Check status is ${check.status}.`,
105
+ PROPOSAL_DESTINATION.CONTROL_CENTER,
106
+ [
107
+ ev("check", `status.checks.${check.name}`, check.status),
108
+ ev("status", "status.overall", status?.overall ?? "failed")
109
+ ]
110
+ ));
111
+ }
112
+ }
113
+ }
114
+
115
+ const matrix = adapters?.adapters ?? status?.agents ?? [];
116
+ for (const entry of matrix) {
117
+ if (!entry?.detected || entry.managed) continue;
118
+ out.push(make(
119
+ `govern-adapter-${slug(entry.id)}`,
120
+ PROPOSAL_SEVERITY.MEDIUM,
121
+ `Govern detected agent: ${entry.id}`,
122
+ `${entry.id} is detected locally but not managed by Kairo.`,
123
+ PROPOSAL_DESTINATION.IDES,
124
+ [
125
+ ev("adapter", `adapters.${entry.id}.detected`, "true"),
126
+ ev("adapter", `adapters.${entry.id}.managed`, "false")
127
+ ]
128
+ ));
129
+ }
130
+
131
+ const effective = policy ?? status?.policy ?? null;
132
+ if (effective?.applyMode && effective.applyMode !== "confirm") {
133
+ out.push(make(
134
+ "review-policy-apply-mode",
135
+ PROPOSAL_SEVERITY.LOW,
136
+ "Review apply mode policy",
137
+ `Effective applyMode is "${effective.applyMode}". Confirm intentional deviation from confirm-gated applies.`,
138
+ PROPOSAL_DESTINATION.PROFILE,
139
+ [ev("policy", "policy.applyMode", String(effective.applyMode))]
140
+ ));
141
+ }
142
+
143
+ for (const check of status?.checks ?? []) {
144
+ if (isIntel(check) || !ACTIONABLE.has(check.status)) continue;
145
+
146
+ if (isExternal(check)) {
147
+ out.push(make(
148
+ `external-${slug(check.name)}`,
149
+ check.status === "missing" || check.status === "failed"
150
+ ? PROPOSAL_SEVERITY.MEDIUM
151
+ : PROPOSAL_SEVERITY.LOW,
152
+ `Review external integration: ${check.name}`,
153
+ externalDetail(check.detail ?? `Check status is ${check.status}.`),
154
+ PROPOSAL_DESTINATION.MODULES,
155
+ [
156
+ ev("check", `status.checks.${check.name}`, check.status),
157
+ ev("integration", "check.category", check.category ?? "integration")
158
+ ]
159
+ ));
160
+ continue;
161
+ }
162
+
163
+ if (check.status !== "warning" && check.status !== "stale") continue;
164
+ const isBackup = check.category === "backups";
165
+ out.push(make(
166
+ `warning-${slug(check.name)}`,
167
+ isBackup ? PROPOSAL_SEVERITY.INFO : PROPOSAL_SEVERITY.LOW,
168
+ isBackup ? `Note: ${check.name}` : `Review note: ${check.name}`,
169
+ check.detail ?? `Check status is ${check.status}.`,
170
+ isBackup ? PROPOSAL_DESTINATION.ACTIVITY : PROPOSAL_DESTINATION.CONTROL_CENTER,
171
+ [ev("check", `status.checks.${check.name}`, check.status)]
172
+ ));
173
+ }
174
+
175
+ return finalize(out);
176
+ }
177
+
178
+ function finalize(candidates) {
179
+ const byId = new Map();
180
+ for (const item of candidates) {
181
+ if (!item?.id || !item.evidence?.length) continue;
182
+ if (!byId.has(item.id)) byId.set(item.id, item);
183
+ }
184
+ return [...byId.values()].sort((left, right) => {
185
+ const rank = (SEVERITY_RANK[left.severity] ?? 99) - (SEVERITY_RANK[right.severity] ?? 99);
186
+ return rank !== 0 ? rank : left.id.localeCompare(right.id);
187
+ });
188
+ }
189
+
190
+ function make(id, severity, title, detail, destination, evidence) {
191
+ return {
192
+ id,
193
+ severity,
194
+ title,
195
+ detail,
196
+ destination,
197
+ evidence: evidence.map((entry) => ({
198
+ type: String(entry.type),
199
+ source: String(entry.source),
200
+ ref: String(entry.ref)
201
+ }))
202
+ };
203
+ }
204
+
205
+ function ev(type, source, ref) {
206
+ return { type, source, ref: String(ref) };
207
+ }
208
+
209
+ function isIntel(check) {
210
+ return check?.category === "intelligence" || check?.name === "intelligence providers";
211
+ }
212
+
213
+ function isExternal(check) {
214
+ const name = check?.name ?? "";
215
+ return name.startsWith("engram:") || name.startsWith("graphify:");
216
+ }
217
+
218
+ function externalDetail(detail) {
219
+ if (/runtime.?active|not runtime/i.test(detail)) return detail;
220
+ return `${detail} Configuration/version/freshness evidence only — not a claim of active runtime.`;
221
+ }
222
+
223
+ function slug(value) {
224
+ return String(value ?? "unknown")
225
+ .toLowerCase()
226
+ .replace(/[^a-z0-9]+/g, "-")
227
+ .replace(/^-|-$/g, "")
228
+ || "unknown";
229
+ }
@@ -0,0 +1,212 @@
1
+ import { buildAdapterMatrixReport } from "./adapter-matrix.js";
2
+ import { listBackupSnapshots } from "./backups.js";
3
+ import { buildDiffReport } from "./diff.js";
4
+ import { buildExplainReport } from "./explain.js";
5
+ import { getHistoryPath, readHistoryEvents } from "./history.js";
6
+ import { harnessHomePaths } from "./paths.js";
7
+ import { buildStatusReport } from "./status.js";
8
+ import { buildControlPlaneJson } from "./json-output.js";
9
+ import { formatCliCommand } from "./brand/cli.js";
10
+ import { buildRuntimeDashboardData } from "./runtime/run-cli.js";
11
+ import { describeBackupSnapshots } from "./rollback.js";
12
+ import { buildControlPlaneProposals } from "./control-plane-proposals.js";
13
+
14
+ /**
15
+ * Global control-plane health. Optional intelligence absence must never
16
+ * degrade these states by itself.
17
+ */
18
+ export const CONTROL_PLANE_HEALTH = {
19
+ NOT_CONFIGURED: "NOT_CONFIGURED",
20
+ ACTION_REQUIRED: "ACTION_REQUIRED",
21
+ HEALTHY_WITH_NOTES: "HEALTHY_WITH_NOTES",
22
+ HEALTHY: "HEALTHY",
23
+ CHECK_FAILED: "CHECK_FAILED"
24
+ };
25
+
26
+ export const CONTROL_PLANE_CTA = {
27
+ SETUP: "setup",
28
+ REPAIR: "repair",
29
+ VERIFY: "verify",
30
+ REVIEW: "review",
31
+ IDLE: "idle"
32
+ };
33
+
34
+ /**
35
+ * Pure mapping from status.overall + warning counts to control-plane health.
36
+ * Does not inspect intelligence backends.
37
+ */
38
+ export function resolveControlPlaneHealth(status) {
39
+ if (!status || status.state == null || status.overall === "missing") {
40
+ return CONTROL_PLANE_HEALTH.NOT_CONFIGURED;
41
+ }
42
+
43
+ if (status.overall === "failed") {
44
+ return CONTROL_PLANE_HEALTH.CHECK_FAILED;
45
+ }
46
+
47
+ if (status.overall === "drift") {
48
+ return CONTROL_PLANE_HEALTH.ACTION_REQUIRED;
49
+ }
50
+
51
+ if ((status.counts?.warning ?? 0) > 0) {
52
+ return CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES;
53
+ }
54
+
55
+ return CONTROL_PLANE_HEALTH.HEALTHY;
56
+ }
57
+
58
+ /**
59
+ * Prefer setup / repairs / verification before launching runs.
60
+ */
61
+ export function resolveControlPlaneCta({ health, status, diff } = {}) {
62
+ switch (health) {
63
+ case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
64
+ return {
65
+ kind: CONTROL_PLANE_CTA.SETUP,
66
+ title: "Finish local setup",
67
+ detail: status?.nextAction
68
+ ?? `Run ${formatCliCommand("setup")} to configure the local ecosystem.`,
69
+ destination: "changes"
70
+ };
71
+ case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
72
+ return {
73
+ kind: CONTROL_PLANE_CTA.REPAIR,
74
+ title: "Review and repair drift",
75
+ detail: status?.nextAction
76
+ ?? `Preview repairs with ${formatCliCommand("diff")}, then confirm sync.`,
77
+ destination: "changes"
78
+ };
79
+ case CONTROL_PLANE_HEALTH.CHECK_FAILED:
80
+ return {
81
+ kind: CONTROL_PLANE_CTA.VERIFY,
82
+ title: "Investigate failed checks",
83
+ detail: status?.nextAction
84
+ ?? `Run ${formatCliCommand("doctor")} for details.`,
85
+ destination: "control-center"
86
+ };
87
+ case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
88
+ return {
89
+ kind: CONTROL_PLANE_CTA.REVIEW,
90
+ title: "Review notes",
91
+ detail: diff?.hasChanges
92
+ ? "Some notes remain after a healthy scan. Open Changes to inspect."
93
+ : "Ecosystem is healthy with non-blocking notes.",
94
+ destination: "control-center"
95
+ };
96
+ case CONTROL_PLANE_HEALTH.HEALTHY:
97
+ return {
98
+ kind: CONTROL_PLANE_CTA.IDLE,
99
+ title: "Ecosystem healthy",
100
+ detail: status?.nextAction ?? "No governance action required.",
101
+ destination: "control-center"
102
+ };
103
+ default: {
104
+ const _exhaustive = health;
105
+ return {
106
+ kind: CONTROL_PLANE_CTA.VERIFY,
107
+ title: "Review control plane",
108
+ detail: String(_exhaustive),
109
+ destination: "control-center"
110
+ };
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Read-only control-plane snapshot composed from existing engines.
117
+ * Never writes. Optional intelligence is intentionally omitted from health.
118
+ */
119
+ export async function buildControlPlaneSnapshot({
120
+ homeDir,
121
+ workspaceRoot = null,
122
+ packageName,
123
+ packageRoot,
124
+ cliVersion,
125
+ includeDiff = true,
126
+ includeExplain = false,
127
+ includeRuntime = true,
128
+ historyLimit = 20
129
+ } = {}) {
130
+ const status = await buildStatusReport(homeDir, { packageRoot, workspaceRoot });
131
+ const adapters = await buildAdapterMatrixReport(homeDir);
132
+ const history = await readHistoryEvents(homeDir, { limit: historyLimit });
133
+ const backupSnapshots = await describeBackupSnapshots(
134
+ harnessHomePaths(homeDir).backupsDir
135
+ ).catch(async () => {
136
+ const names = await listBackupSnapshots(harnessHomePaths(homeDir).backupsDir);
137
+ return names.map((name) => ({ name, fileCount: null }));
138
+ });
139
+
140
+ const diff = includeDiff
141
+ ? await buildDiffReport(homeDir, {
142
+ packageRoot,
143
+ packageName,
144
+ cliVersion,
145
+ workspaceRoot
146
+ })
147
+ : null;
148
+
149
+ const explain = includeExplain
150
+ ? await buildExplainReport(homeDir)
151
+ : null;
152
+
153
+ const runtime = includeRuntime
154
+ ? await buildRuntimeDashboardData({ homeDir, workspaceRoot, cliVersion })
155
+ : null;
156
+
157
+ const health = resolveControlPlaneHealth(status);
158
+ const cta = resolveControlPlaneCta({ health, status, diff });
159
+ const proposals = buildControlPlaneProposals({
160
+ health,
161
+ status,
162
+ adapters,
163
+ policy: status.policy,
164
+ diff
165
+ });
166
+
167
+ const governedAgents = (adapters.adapters ?? []).filter((entry) => entry.managed).length;
168
+ const detectedAgents = (adapters.adapters ?? []).filter((entry) => entry.detected).length;
169
+
170
+ return {
171
+ readOnly: true,
172
+ scannedAt: new Date().toISOString(),
173
+ cliVersion,
174
+ packageName,
175
+ homeDir,
176
+ workspaceRoot,
177
+ health,
178
+ cta,
179
+ proposals,
180
+ coverage: {
181
+ detectedAgents,
182
+ governedAgents,
183
+ components: status.components?.length ?? 0,
184
+ activeModules: (status.components ?? [])
185
+ .filter((entry) => entry.status === "ok")
186
+ .map((entry) => entry.id)
187
+ },
188
+ status,
189
+ adapters,
190
+ policy: status.policy,
191
+ backups: {
192
+ count: status.backups,
193
+ snapshots: backupSnapshots
194
+ },
195
+ history: {
196
+ path: getHistoryPath(homeDir),
197
+ limit: historyLimit,
198
+ events: history.events,
199
+ warnings: history.warnings
200
+ },
201
+ diff,
202
+ explain,
203
+ runtime: runtime
204
+ ? {
205
+ activeRuns: runtime.activeRuns?.length ?? 0,
206
+ recentRuns: runtime.recentRuns?.length ?? 0,
207
+ providers: runtime.providers ?? []
208
+ }
209
+ : null,
210
+ envelope: buildControlPlaneJson(status, { cliVersion })
211
+ };
212
+ }
@@ -42,7 +42,11 @@ export function countLaunchableAgents(dashboard = null) {
42
42
 
43
43
  export function hasIntelligenceConfigured(diagnostics = null) {
44
44
  const summary = diagnostics?.intelligence?.summary;
45
- return Boolean(summary?.localAvailable || summary?.cloudAuthenticated);
45
+ return Boolean(
46
+ summary?.localAvailable
47
+ || summary?.cloudConfigured
48
+ || summary?.cloudAuthenticated
49
+ );
46
50
  }
47
51
 
48
52
  function hasBlockingDiagnostics(diagnostics = null) {
@@ -174,17 +178,37 @@ export function resolveDashboardRecommendation({
174
178
  };
175
179
  }
176
180
 
177
- if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
181
+ const intelConfigured = Boolean(
182
+ intelligence?.localAvailable
183
+ || intelligence?.cloudConfigured
184
+ || intelligence?.cloudAuthenticated
185
+ );
186
+ if (!intelConfigured) {
178
187
  return {
179
188
  kind: NEXT_STEP_KINDS.ENABLE_INTELLIGENCE,
180
189
  title: "Enable optional intelligence",
181
- message: "Enable intelligence: start Ollama or set OPENROUTER_API_KEY, then retry.",
190
+ message: "Enable intelligence: start Ollama, install OpenCode CLI, or set OPENCODE_API_KEY / OPENROUTER_API_KEY, then retry.",
182
191
  detail: "Agents can still run without this optional capability.",
183
192
  targetView: RECOMMENDATION_TARGETS.DIAGNOSTICS,
184
193
  targetAction: null
185
194
  };
186
195
  }
187
196
 
197
+ if (
198
+ !intelligence?.localAvailable
199
+ && intelligence?.cloudConfigured
200
+ && !intelligence?.cloudAuthenticated
201
+ ) {
202
+ return {
203
+ kind: NEXT_STEP_KINDS.REVIEW,
204
+ title: "Inspect intelligence authentication",
205
+ message: `Cloud intelligence is configured but not authenticated. Run ${formatCliCommand("intelligence status")}.`,
206
+ detail: "Confirm API key or OpenCode auth evidence before cloud invoke.",
207
+ targetView: RECOMMENDATION_TARGETS.DIAGNOSTICS,
208
+ targetAction: null
209
+ };
210
+ }
211
+
188
212
  return {
189
213
  kind: NEXT_STEP_KINDS.REVIEW,
190
214
  title: "Review system health",
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { updateGlobalHarness } from "./global-installer.js";
5
+ import { needsManagedRepair } from "./governance-repair.js";
5
6
  import { harnessHomePaths } from "./paths.js";
6
7
  import { readGlobalState } from "./state.js";
7
8
  import { buildStatusReport } from "./status.js";
@@ -31,11 +32,7 @@ export async function buildDiffReport(homeDir, {
31
32
  }
32
33
 
33
34
  const statusReport = await buildStatusReport(homeDir, { packageRoot, workspaceRoot });
34
- const needsRepair = statusReport.overall === "drift"
35
- || statusReport.counts.missing > 0
36
- || statusReport.counts.stale > 0;
37
-
38
- if (!needsRepair) {
35
+ if (!needsManagedRepair(statusReport)) {
39
36
  return {
40
37
  homeDir,
41
38
  installed: true,
@@ -404,7 +404,7 @@ export async function runGlobalDoctor(packageRoot, {
404
404
  return report;
405
405
  }
406
406
 
407
- const { checks, ok, hasDrift } = await runGlobalDoctorChecks(homeDir, { packageRoot, workspaceRoot });
407
+ const { checks, ok, hasDrift, componentHealth } = await runGlobalDoctorChecks(homeDir, { packageRoot, workspaceRoot });
408
408
 
409
409
  console.log(`${BRAND.displayName} doctor (scope: agent-global)`);
410
410
  console.log(`Home: ${homeDir}`);
@@ -416,6 +416,14 @@ export async function runGlobalDoctor(packageRoot, {
416
416
  console.log(`[${label}] ${check.name}${detail}`);
417
417
  }
418
418
 
419
+ if (componentHealth.length > 0) {
420
+ console.log("");
421
+ console.log("Components:");
422
+ for (const entry of componentHealth) {
423
+ console.log(` ${entry.id.padEnd(16)} ${entry.status}`);
424
+ }
425
+ }
426
+
419
427
  console.log("");
420
428
  if (ok) {
421
429
  console.log("Status: OK");
@@ -782,7 +790,14 @@ function printComponentEntry(component, { workspace = false } = {}) {
782
790
  console.log("");
783
791
  console.log(`${component.id} (${component.version}) [${defaultLabel}]`);
784
792
  console.log(` Label: ${component.label}`);
793
+ console.log(` Kind: ${component.kind ?? "component"}`);
785
794
  console.log(` Assets: ${component.assetFiles.join(", ")}`);
795
+ if ((component.dependencies ?? []).length > 0) {
796
+ console.log(` Dependencies: ${component.dependencies.join(", ")}`);
797
+ }
798
+ if ((component.healthChecks ?? []).length > 0) {
799
+ console.log(` Health checks: ${component.healthChecks.map((check) => check.id).join(", ")}`);
800
+ }
786
801
 
787
802
  if (component.instructions) {
788
803
  console.log(` Instructions: ${component.instructions}`);
@@ -1,13 +1,24 @@
1
1
  import { listBackupSnapshots } from "./backups.js";
2
2
  import { buildAdapterContext } from "./adapter-context.js";
3
+ import { buildComponentHealthEntries } from "./component-health.js";
3
4
  import { runComponentEcosystemChecks } from "./component-ecosystem-checks.js";
4
5
  import { detectGlobalDrift, hasRepairableDrift } from "./drift.js";
5
6
  import { harnessHomePaths } from "./paths.js";
6
7
  import { resolveComponent } from "./component-registry.js";
7
8
  import { readGlobalState } from "./state.js";
8
9
  import { formatCliCommand } from "./brand/cli.js";
10
+ import {
11
+ BACKEND_IDS,
12
+ inspectIntelligenceBackends
13
+ } from "./intelligence/index.js";
9
14
 
10
- export async function runGlobalDoctorChecks(homeDir, { packageRoot, workspaceRoot = null } = {}) {
15
+ export async function runGlobalDoctorChecks(homeDir, {
16
+ packageRoot,
17
+ workspaceRoot = null,
18
+ env = process.env,
19
+ fetchImpl = globalThis.fetch,
20
+ inspectBackends = inspectIntelligenceBackends
21
+ } = {}) {
11
22
  const paths = harnessHomePaths(homeDir);
12
23
  const state = await readGlobalState(paths.statePath);
13
24
  const installedComponents = (state?.components ?? []).map((entry) => resolveComponent(entry.id, { workspaceRoot }));
@@ -26,22 +37,84 @@ export async function runGlobalDoctorChecks(homeDir, { packageRoot, workspaceRoo
26
37
  checks.push(await backupsCheck(paths));
27
38
  checks.push(...await runComponentEcosystemChecks({
28
39
  installedComponents,
29
- workspaceRoot
40
+ workspaceRoot,
41
+ homeDir
30
42
  }));
31
43
  }
32
44
 
45
+ checks.push(await intelligenceProvidersCheck({ env, fetchImpl, inspectBackends }));
46
+
33
47
  const hasMissing = checks.some((check) => check.status === "missing");
34
48
  const hasStale = checks.some((check) => check.status === "stale");
49
+ const componentHealth = buildComponentHealthEntries(state?.components ?? [], checks);
35
50
 
36
51
  return {
37
52
  checks,
38
53
  ok: !hasMissing && !hasStale,
39
54
  hasDrift: hasRepairableDrift(checks),
55
+ componentHealth,
40
56
  state,
41
57
  paths
42
58
  };
43
59
  }
44
60
 
61
+ async function intelligenceProvidersCheck({ env, fetchImpl, inspectBackends }) {
62
+ try {
63
+ const backends = await inspectBackends({ env, fetchImpl });
64
+ const parts = [];
65
+
66
+ if (backends.some((entry) => entry.id === BACKEND_IDS.OLLAMA && entry.available)) {
67
+ parts.push("Ollama available");
68
+ }
69
+
70
+ const go = backends.find((entry) => entry.id === BACKEND_IDS.OPENCODE_GO);
71
+ const zen = backends.find((entry) => entry.id === BACKEND_IDS.OPENCODE_ZEN);
72
+ const runtime = backends.find((entry) => entry.id === BACKEND_IDS.OPENCODE);
73
+
74
+ if (go?.configured || go?.hasApiKey) {
75
+ parts.push(formatCloudProvider("OpenCode Go", go));
76
+ }
77
+ if (zen?.configured || zen?.hasApiKey) {
78
+ parts.push(formatCloudProvider("OpenCode Zen", zen));
79
+ }
80
+ if (runtime?.available || runtime?.detected) {
81
+ const providers = runtime.evidence?.authProviders?.length
82
+ ? runtime.evidence.authProviders.join(", ")
83
+ : "CLI installed";
84
+ parts.push(`OpenCode CLI runtime (${providers}; not authenticated by Kairo)`);
85
+ }
86
+ if (backends.some((entry) => entry.id === BACKEND_IDS.OPENROUTER && entry.hasApiKey)) {
87
+ parts.push("OpenRouter configured (env key; entitlement unverified)");
88
+ }
89
+
90
+ return {
91
+ name: "intelligence providers",
92
+ status: "ok",
93
+ category: "intelligence",
94
+ detail: parts.length > 0
95
+ ? `${parts.join("; ")}. Cloud invoke still needs --cloud-consent --yes. API keys never prove subscription or balance.`
96
+ : `Optional. Start Ollama, install OpenCode CLI, or set OPENCODE_API_KEY / OPENROUTER_API_KEY. See ${formatCliCommand("intelligence status")}.`
97
+ };
98
+ } catch (error) {
99
+ return {
100
+ name: "intelligence providers",
101
+ status: "warning",
102
+ category: "intelligence",
103
+ detail: `Intelligence provider inspection failed (${error?.message ?? error}). Overall doctor health is unchanged.`
104
+ };
105
+ }
106
+ }
107
+
108
+ function formatCloudProvider(label, entry) {
109
+ const identity = entry.authenticated
110
+ ? "authenticated"
111
+ : entry.configured || entry.hasApiKey
112
+ ? "configured"
113
+ : "detected";
114
+ const entitlement = entry.entitlement ?? "unknown";
115
+ return `${label} (${identity}; entitlement=${entitlement})`;
116
+ }
117
+
45
118
  function stateOnlyCheck(state) {
46
119
  if (!state) {
47
120
  return {