@kal-elsam/kairo-runtime 0.4.2 → 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 +22 -1
  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,34 @@
1
+ ---
2
+ name: sdd-tasks
3
+ description: "Break an accepted design into ordered, reviewable tasks."
4
+ ---
5
+
6
+ # SDD Tasks
7
+
8
+ Use this skill after design to produce a task list that one reviewer can follow without reopening scope.
9
+
10
+ ## References
11
+ - [Phase contract](references/contract.md)
12
+ ## Source of truth
13
+
14
+ - `AGENTS.md`
15
+ - `docs/ai/harness.md`
16
+ - `docs/ai/spec-driven-development.md`
17
+ - `docs/ai/testing.md`
18
+
19
+ ## Workflow
20
+
21
+ 1. Split the design into independent, reviewable units.
22
+ 2. Order tasks by dependency and risk.
23
+ 3. Attach acceptance and validation to each task.
24
+ 4. Keep persona and SDD contracts out of the task bodies.
25
+
26
+ ## Output
27
+
28
+ ```txt
29
+ Task order:
30
+ Dependencies:
31
+ Acceptance per task:
32
+ Validation per task:
33
+ Next skill:
34
+ ```
@@ -0,0 +1,4 @@
1
+ # sdd-tasks contract
2
+ Activation: After design is stable enough to decompose. Hard rules: Independent reviewable units with acceptance+validation; no persona in bodies.
3
+ Gates: Ordered list ready → sdd-apply · design hole → return to sdd-design Steps: Split units → order by dependency/risk → attach acceptance/validation → stop.
4
+ Output: Task order | Dependencies | Acceptance per task | Validation per task | Next skill
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: sdd-verify
3
+ description: "Validate applied work against acceptance and pipeline gates."
4
+ ---
5
+
6
+ # SDD Verify
7
+
8
+ Use this skill after implementation to prove the task is done with evidence, not claims.
9
+
10
+ ## References
11
+ - [Phase contract](references/contract.md)
12
+ ## Source of truth
13
+
14
+ - `AGENTS.md`
15
+ - `docs/ai/harness.md`
16
+ - `docs/ai/quality-gates.md`
17
+ - `docs/ai/testing.md`
18
+
19
+ ## Workflow
20
+
21
+ 1. Re-read acceptance criteria for the completed task.
22
+ 2. Run the relevant lint, type, test, and smoke checks.
23
+ 3. Record failures, skips, and residual risk honestly.
24
+ 4. Hand off to archive only when the evidence is complete.
25
+
26
+ ## Output
27
+
28
+ ```txt
29
+ Acceptance checked:
30
+ Commands run:
31
+ Results:
32
+ Residual risk:
33
+ Next skill:
34
+ ```
@@ -0,0 +1,4 @@
1
+ # sdd-verify contract
2
+ Activation: After apply, before archive — prove done with evidence. Hard rules: Evidence over claims; record skips/failures/residual risk honestly.
3
+ Gates: Evidence complete → sdd-archive · acceptance fails → return to sdd-apply Steps: Re-read acceptance → run lint/type/test/smoke → record results → handoff.
4
+ Output: Acceptance checked | Commands run | Results | Residual risk | Next skill
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Gemini, Copilot, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
@@ -19,13 +19,14 @@ import {
19
19
  reduceCockpitUi,
20
20
  routeCockpitKey
21
21
  } from "../src/global/ink/cockpit-controller.js";
22
+ import { formatProposalLines } from "../src/global/ink/cockpit-proposals.js";
22
23
  import { ORCHESTRATOR_VIEWS } from "../src/global/ink/orchestrator-state.js";
23
24
 
24
25
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
25
26
  const require = createRequire(import.meta.url);
26
27
  const pkg = require(join(root, "package.json"));
27
28
 
28
- assert.equal(pkg.version, "0.4.2");
29
+ assert.equal(pkg.version, "0.5.0");
29
30
  assert.ok(pkg.dependencies["ansi-escapes"]);
30
31
 
31
32
  assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
@@ -68,7 +69,7 @@ const mission = buildHomeMissionModel({
68
69
  },
69
70
  layoutMode: LAYOUT_MODES.MINIMAL
70
71
  });
71
- assert.match(mission.title, /HOME — smoke/);
72
+ assert.match(mission.title, /HOME — smoke|CONTROL CENTER/i);
72
73
  assert.match(mission.readiness.headline, /LIMITED|READY|NEEDS/i);
73
74
  assert.equal(mission.next.targetAction, "launch");
74
75
  assert.match(mission.recent.headline, /Codex · Failed/);
@@ -84,7 +85,7 @@ const asciiNav = buildNavModel({
84
85
  assert.equal(asciiNav.items[1].marker, ">");
85
86
  assert.equal(asciiNav.items[0].current, true);
86
87
  assert.equal(asciiNav.items[1].selected, true);
87
- assert.match(asciiNav.explanation, /Supervised runs|Running/i);
88
+ assert.match(asciiNav.explanation, /Detected agents|IDEs|models|Harness|modules/i);
88
89
 
89
90
  function applyKey(state, keyAction) {
90
91
  const routed = routeCockpitKey(state, keyAction);
@@ -97,21 +98,21 @@ function smokeNavigation(layoutMode) {
97
98
  layoutMode,
98
99
  region: layoutMode === LAYOUT_MODES.MINIMAL ? COCKPIT_REGIONS.CONTENT : COCKPIT_REGIONS.NAV
99
100
  });
100
- const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "diagnostics");
101
+ const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "changes");
101
102
  while (state.navIndex < diagnosticsIndex) {
102
103
  state = applyKey(state, { type: "arrow", direction: "down" });
103
104
  }
104
105
  state = applyKey(state, { type: "enter" });
105
- assert.equal(state.view, ORCHESTRATOR_VIEWS.DIAGNOSTICS);
106
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.CHANGES);
106
107
  assert.equal(state.region, COCKPIT_REGIONS.NAV);
107
108
 
108
109
  state = applyKey(state, { type: "arrow", direction: "up" });
109
110
  state = applyKey(state, { type: "enter" });
110
- assert.equal(state.view, ORCHESTRATOR_VIEWS.LAUNCH);
111
- assert.equal(state.region, COCKPIT_REGIONS.CONTENT);
111
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.MODULES);
112
+ assert.equal(state.region, COCKPIT_REGIONS.NAV);
112
113
 
113
114
  const footer = buildFooterModel({
114
- view: ORCHESTRATOR_VIEWS.DIAGNOSTICS,
115
+ view: ORCHESTRATOR_VIEWS.CHANGES,
115
116
  region: COCKPIT_REGIONS.NAV,
116
117
  unicode: false
117
118
  });
@@ -126,4 +127,24 @@ function smokeNavigation(layoutMode) {
126
127
  smokeNavigation(LAYOUT_MODES.WIDE);
127
128
  smokeNavigation(LAYOUT_MODES.COMPACT);
128
129
 
130
+ const proposalLines = formatProposalLines([
131
+ {
132
+ id: "setup-local",
133
+ severity: "high",
134
+ title: "Finish local setup",
135
+ destination: "changes",
136
+ evidence: [{ type: "status", source: "status.overall", ref: "missing" }]
137
+ }
138
+ ], {
139
+ budgets: {
140
+ stableUsedTokens: 1,
141
+ stableBudgetTokens: 10,
142
+ requestUsedTokens: 0,
143
+ requestBudgetTokens: 10
144
+ }
145
+ });
146
+ assert.match(proposalLines.join("\n"), /\[HIGH\] Finish local setup → changes/);
147
+ assert.match(proposalLines.join("\n"), /Budget · stable 1\/10/);
148
+ assert.match(proposalLines.join("\n"), /evidence: status\.overall/);
149
+
129
150
  console.log("cockpit smoke OK");
@@ -78,6 +78,15 @@ echo
78
78
  echo "== kairo status (after install) =="
79
79
  npx --no-install kairo status
80
80
 
81
+ echo
82
+ echo "== kairo components configure/verify sdd-core =="
83
+ npx --no-install kairo components configure sdd-core --agents codex,cursor --persona off --dry-run --json \
84
+ | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{const p=JSON.parse(s);if(p.writes!==false||p.applied!==false)process.exit(1)})'
85
+ npx --no-install kairo components configure sdd-core --agents codex,cursor --persona off --yes --json \
86
+ | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{const p=JSON.parse(s);const fs=require("fs");const path=require("path");if(p.writes!==false||(p.summary?.create??0)!==0||!p.receipt?.id)process.exit(1);if(!fs.existsSync(path.join(process.env.HARNESS_HOME,".agents","skills","sdd-init","SKILL.md")))process.exit(1)})'
87
+ npx --no-install kairo components verify sdd-core --agents codex,cursor --json \
88
+ | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{const p=JSON.parse(s);if(p.status!=="configured"||p.ok!==true)process.exit(1)})'
89
+
81
90
  SNAPSHOT="$(ls -A "$FAKE_HOME/.harness/backups" | head -1)"
82
91
  BACKUP_FILE="$(ls -A "$FAKE_HOME/.harness/backups/$SNAPSHOT" | head -1)"
83
92
 
package/src/cli.js CHANGED
@@ -4,6 +4,11 @@ import { basename, dirname, resolve } from "node:path";
4
4
  import { ADAPTERS } from "./harness-files.js";
5
5
  import { GLOBAL_AGENT_IDS } from "./global/registry.js";
6
6
  import { COMPONENT_IDS, DEFAULT_COMPONENT_IDS } from "./global/component-registry.js";
7
+ import {
8
+ runComponentsConfigure,
9
+ runComponentsRollback,
10
+ runComponentsVerify
11
+ } from "./global/component-integration-cli.js";
7
12
  import {
8
13
  printGlobalComponents,
9
14
  printGlobalDetect,
@@ -237,6 +242,15 @@ async function dispatchComponentsCommand(options, invoke) {
237
242
  case "import":
238
243
  await runComponentsImport(options);
239
244
  return;
245
+ case "configure":
246
+ await runComponentsConfigure({ ...options, packageRoot });
247
+ return;
248
+ case "verify":
249
+ await runComponentsVerify({ ...options, packageRoot });
250
+ return;
251
+ case "rollback":
252
+ await runComponentsRollback(options);
253
+ return;
240
254
  default:
241
255
  throw new Error(
242
256
  `Unknown components action "${options.componentsAction}". Run "${invoke} help".`
@@ -321,6 +335,8 @@ export function parseArgs(argv) {
321
335
  components: null,
322
336
  componentsAction: null,
323
337
  componentId: null,
338
+ receiptId: null,
339
+ persona: null,
324
340
  label: null,
325
341
  outPath: null,
326
342
  bundlePath: null,
@@ -358,6 +374,7 @@ export function parseArgs(argv) {
358
374
  agent: null,
359
375
  task: null,
360
376
  model: null,
377
+ intelligenceBackend: null,
361
378
  permissions: null,
362
379
  captureTranscript: false,
363
380
  follow: false,
@@ -438,6 +455,10 @@ export function parseArgs(argv) {
438
455
  } else if (arg === "--apply") options.apply = true;
439
456
  else if (arg === "--to") options.snapshot = args[++index];
440
457
  else if (arg.startsWith("--to=")) options.snapshot = arg.slice("--to=".length);
458
+ else if (arg === "--receipt") options.receiptId = args[++index];
459
+ else if (arg.startsWith("--receipt=")) options.receiptId = arg.slice("--receipt=".length);
460
+ else if (arg === "--persona") options.persona = parsePersona(args[++index]);
461
+ else if (arg.startsWith("--persona=")) options.persona = parsePersona(arg.slice("--persona=".length));
441
462
  else if (arg === "--limit") options.limit = parsePositiveInt(args[++index], "limit");
442
463
  else if (arg.startsWith("--limit=")) options.limit = parsePositiveInt(arg.slice("--limit=".length), "limit");
443
464
  else if (arg === "--command") options.historyCommand = args[++index];
@@ -462,6 +483,8 @@ export function parseArgs(argv) {
462
483
  else if (arg.startsWith("--agent=")) options.agent = arg.slice("--agent=".length);
463
484
  else if (arg === "--model") options.model = args[++index];
464
485
  else if (arg.startsWith("--model=")) options.model = arg.slice("--model=".length);
486
+ else if (arg === "--backend") options.intelligenceBackend = args[++index] ?? "";
487
+ else if (arg.startsWith("--backend=")) options.intelligenceBackend = arg.slice("--backend=".length);
465
488
  else if (arg === "--permissions") options.permissions = parsePathList(args[++index]);
466
489
  else if (arg.startsWith("--permissions=")) options.permissions = parsePathList(arg.slice("--permissions=".length));
467
490
  else if (arg === "--capture-transcript") options.captureTranscript = true;
@@ -514,7 +537,18 @@ function parseComponentsAction(args, options) {
514
537
 
515
538
  if (action === "validate") return;
516
539
 
517
- throw new Error(`Unknown components action "${action}". Use validate, init, pack, or import.`);
540
+ if (action === "configure" || action === "verify" || action === "rollback") {
541
+ const componentId = args[0];
542
+ if (!componentId || componentId.startsWith("-")) {
543
+ throw new Error(
544
+ `Missing component id. Use: ${formatCliCommand(`components ${action} <component-id>`)}`
545
+ );
546
+ }
547
+ options.componentId = args.shift();
548
+ return;
549
+ }
550
+
551
+ throw new Error(`Unknown components action "${action}". Use validate, init, pack, import, configure, verify, or rollback.`);
518
552
  }
519
553
 
520
554
  function parsePolicyAction(args, options) {
@@ -685,6 +719,14 @@ function parseAdapters(value) {
685
719
  return items;
686
720
  }
687
721
 
722
+ function parsePersona(value) {
723
+ const persona = String(value ?? "").trim().toLowerCase();
724
+ if (persona !== "off" && persona !== "teaching") {
725
+ throw new Error('Invalid --persona. Use "off" or "teaching".');
726
+ }
727
+ return persona;
728
+ }
729
+
688
730
  function printHelp() {
689
731
  const cli = PREFERRED_CLI;
690
732
  console.log(`${BRAND.displayName} (${PACKAGE_NAME})
@@ -707,7 +749,9 @@ Usage:
707
749
  ${cli} runs stop <runId> [--json]
708
750
  ${cli} orchestrator [--json] Read-only agent capability diagnostics
709
751
  ${cli} intelligence [status|models|context|route|ask] [--json]
710
- ${cli} intelligence ask --prompt "..." [--cloud-consent] [--yes] [--paths a,b]
752
+ ${cli} intelligence models --backend opencode-go|opencode-zen|opencode
753
+ ${cli} intelligence route --backend opencode-go --model <id> [--cloud-consent]
754
+ ${cli} intelligence ask --prompt "..." [--backend <id>] [--model <id>] [--cloud-consent] [--yes] [--paths a,b]
711
755
  ${cli} setup [--dry-run] [--yes] [--confirm] [--simple] [--no-preflight] [--agents <list|all>] [--components <list>]
712
756
  ${cli} status [--json]
713
757
  ${cli} sync [--dry-run] [--yes] [--confirm] [--json] [--no-preflight]
@@ -732,6 +776,10 @@ Usage:
732
776
  ${cli} report [--json] [--out <file>] [--limit <n>]
733
777
  ${cli} components
734
778
  ${cli} components validate|init|pack|import ...
779
+ ${cli} components configure engram-memory [--agents <list>] [--dry-run|--yes] [--json]
780
+ ${cli} components configure sdd-core [--agents <list>] [--persona off|teaching] [--dry-run|--yes] [--json]
781
+ ${cli} components verify sdd-core [--agents <list>] [--json]
782
+ ${cli} components rollback engram-memory|sdd-core --receipt <id> [--dry-run|--yes] [--json]
735
783
  ${cli} uninstall [--dry-run]
736
784
 
737
785
  Scopes:
@@ -749,8 +797,9 @@ Commands:
749
797
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
750
798
  orchestrator Read-only capability registry diagnostics (--json supported).
751
799
  intelligence Harness Engineering layer: backends, context packs, routing, budgets.
752
- Local-first (Ollama). Cloud (OpenRouter/free) only with --cloud-consent.
753
- Credentials via env only (OPENROUTER_API_KEY, OLLAMA_HOST). Never stored.
800
+ Local-first (Ollama). Cloud only with --cloud-consent.
801
+ Ephemeral --backend/--model override preferredBackend/preferredModel.
802
+ Credentials via env only (OPENROUTER_API_KEY, OPENCODE_API_KEY, OLLAMA_HOST). Never stored.
754
803
  setup Managed ecosystem setup. Interactive Ink UI (TTY). Use --simple for Clack prompts.
755
804
  status Control panel: agents, components, drift, backups, next action.
756
805
  sync Converge managed content (repair drift), then show status.
@@ -768,7 +817,7 @@ Commands:
768
817
  rollback Preview or restore a prior config snapshot (--apply to write).
769
818
  policy View or edit local operation preferences under ~/.harness/policy.json.
770
819
  report Read-only diagnostics bundle: status, policy, adapters, diff, history.
771
- components List, validate, scaffold, pack, or import workspace components.
820
+ components List, validate, scaffold, pack, import, or configure integrations (Engram, SDD).
772
821
  uninstall Remove managed sections and global state. Backups are preserved.
773
822
  init Alias for install --scope=workspace (legacy).
774
823
 
@@ -180,7 +180,7 @@ function summarizeDiagnostics(capabilities) {
180
180
  };
181
181
  }
182
182
 
183
- function buildDiagnosticRecommendations({
183
+ export function buildDiagnosticRecommendations({
184
184
  capabilities,
185
185
  profile,
186
186
  diagnostics,
@@ -206,13 +206,26 @@ function buildDiagnosticRecommendations({
206
206
  }
207
207
  }
208
208
 
209
- if (!intelligenceSummary?.localAvailable && !intelligenceSummary?.cloudAuthenticated) {
209
+ if (!intelligenceSummary?.localAvailable && !intelligenceSummary?.cloudConfigured) {
210
210
  recommendations.push(
211
- "No intelligence backend available. Start Ollama or set OPENROUTER_API_KEY (env only). Diagnostics mode remains available."
211
+ "No intelligence backend available. Start Ollama, install the OpenCode CLI, or set OPENCODE_API_KEY / OPENROUTER_API_KEY (env only). Diagnostics mode remains available."
212
+ );
213
+ } else if (
214
+ !intelligenceSummary?.localAvailable
215
+ && intelligenceSummary?.cloudConfigured
216
+ && !intelligenceSummary?.cloudAuthenticated
217
+ ) {
218
+ recommendations.push(
219
+ `Cloud intelligence is configured but not authenticated. Run ${formatCliCommand("intelligence status")} to inspect evidence before invoke.`
212
220
  );
213
221
  } else if (!intelligenceSummary?.localAvailable && intelligenceSummary?.cloudAuthenticated) {
222
+ const provider = intelligenceSummary.opencodeGoAuthenticated
223
+ ? "OpenCode Go"
224
+ : intelligenceSummary.opencodeZenAuthenticated
225
+ ? "OpenCode Zen"
226
+ : "OpenRouter";
214
227
  recommendations.push(
215
- `OpenRouter is authenticated. Cloud invoke still requires session flags: ${formatCliCommand("intelligence ask --cloud-consent --yes")}.`
228
+ `${provider} is authenticated. Cloud invoke requires: ${formatCliCommand("intelligence ask --cloud-consent --yes")}.`
216
229
  );
217
230
  }
218
231
 
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
+ import { COMPONENT_ID_PATTERN } from "./component-manifest.js";
4
5
  import { COMPONENT_IDS } from "./component-registry.js";
5
6
  import {
6
7
  hasWorkspaceCatalog,
@@ -9,8 +10,6 @@ import {
9
10
  workspaceCatalogPath
10
11
  } from "./load-workspace-component-catalog.js";
11
12
  import { formatCliCommand } from "./brand/cli.js";
12
-
13
- const COMPONENT_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
14
13
  const DEFAULT_VERSION = "0.1.0";
15
14
  const DEFAULT_ASSET = "README.md";
16
15
 
@@ -1,17 +1,34 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
- import { join, resolve } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { formatCliCommand } from "./brand/cli.js";
6
-
7
+ import { ensureIntegrationProvidersRegistered } from "./integrations/index.js";
8
+ import { requireIntegrationProvider } from "./integrations/provider-registry.js";
9
+ import { buildEngramIntegrationChecks, buildSddIntegrationChecks } from "./component-integration-cli.js";
10
+ import { KAIRO_ENGRAM_AGENT_IDS } from "./integrations/engram-evidence.js";
11
+ import { SDD_MANAGED_AGENT_IDS } from "./integrations/sdd-destinations.js";
12
+ import { harnessHomePaths } from "./paths.js";
13
+ import { readGlobalState } from "./state.js";
14
+
15
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
7
16
  const GRAPH_REPORT_COMMIT_PATTERN = /Built from commit:\s*`([0-9a-f]+)`/i;
8
17
 
9
- export async function runComponentEcosystemChecks({ installedComponents, workspaceRoot = null } = {}) {
18
+ export async function runComponentEcosystemChecks({
19
+ installedComponents,
20
+ workspaceRoot = null,
21
+ homeDir = null
22
+ } = {}) {
10
23
  const installedIds = new Set(installedComponents.map((component) => component.id));
11
24
  const checks = [];
12
25
 
13
26
  if (installedIds.has("engram-memory")) {
14
- checks.push(buildEngramMcpCheck());
27
+ checks.push(...await buildEngramChecks({ homeDir }));
28
+ }
29
+
30
+ if (installedIds.has("sdd-core")) {
31
+ checks.push(...await buildSddChecks({ homeDir }));
15
32
  }
16
33
 
17
34
  if (installedIds.has("graphify-context")) {
@@ -21,14 +38,59 @@ export async function runComponentEcosystemChecks({ installedComponents, workspa
21
38
  return checks;
22
39
  }
23
40
 
24
- function buildEngramMcpCheck() {
25
- return {
26
- name: "engram:mcp-tools",
27
- status: "warning",
28
- category: "integration",
29
- componentId: "engram-memory",
30
- detail: "Engram MCP is not bundled. Configure mem_* tools in your agent (user-engram or plugin-engram-engram) when ready."
31
- };
41
+ async function buildEngramChecks({ homeDir }) {
42
+ ensureIntegrationProvidersRegistered();
43
+ try {
44
+ const provider = requireIntegrationProvider("engram");
45
+ const inspection = await provider.inspect({
46
+ homeDir: homeDir ?? undefined,
47
+ agentIds: KAIRO_ENGRAM_AGENT_IDS
48
+ });
49
+ return buildEngramIntegrationChecks(inspection);
50
+ } catch (error) {
51
+ return [{
52
+ name: "engram:binary",
53
+ status: "warning",
54
+ category: "integration",
55
+ componentId: "engram-memory",
56
+ detail: error instanceof Error ? error.message : "Engram inspection failed."
57
+ }];
58
+ }
59
+ }
60
+
61
+ async function buildSddChecks({ homeDir }) {
62
+ ensureIntegrationProvidersRegistered();
63
+ try {
64
+ const provider = requireIntegrationProvider("sdd-core");
65
+ const resolvedHome = homeDir ?? undefined;
66
+ const state = resolvedHome
67
+ ? await readGlobalState(harnessHomePaths(resolvedHome).statePath)
68
+ : null;
69
+ const managed = new Set(SDD_MANAGED_AGENT_IDS);
70
+ const installedAgents = (state?.adapters ?? [])
71
+ .map((entry) => entry.id)
72
+ .filter((id) => managed.has(id));
73
+ const trackedFiles = Object.fromEntries(
74
+ (state?.sdd?.files ?? []).map((file) => [file.destinationPath, file.hash])
75
+ );
76
+ const verification = await provider.verify({
77
+ homeDir: resolvedHome,
78
+ requestedAgentIds: installedAgents.length ? installedAgents : [],
79
+ detectedAgentIds: installedAgents,
80
+ trackedFiles,
81
+ personaAgentIds: state?.sdd?.personaAgentIds ?? [],
82
+ packageRoot: PACKAGE_ROOT
83
+ });
84
+ return buildSddIntegrationChecks(verification);
85
+ } catch (error) {
86
+ return [{
87
+ name: "sdd-core:skills",
88
+ status: "warning",
89
+ category: "integration",
90
+ componentId: "sdd-core",
91
+ detail: error instanceof Error ? error.message : "SDD verification failed."
92
+ }];
93
+ }
32
94
  }
33
95
 
34
96
  async function buildGraphifyChecks(workspaceRoot) {
@@ -115,7 +177,6 @@ async function readGraphFreshness(workspaceRoot, reportPath) {
115
177
  }
116
178
 
117
179
  const isFresh = headCommit.startsWith(graphCommit) || graphCommit.startsWith(headCommit);
118
-
119
180
  if (isFresh) {
120
181
  return {
121
182
  status: "ok",
@@ -134,9 +195,7 @@ function resolveGitHead(workspaceRoot) {
134
195
  cwd: workspaceRoot,
135
196
  encoding: "utf8"
136
197
  });
137
-
138
198
  if (result.status !== 0) return null;
139
-
140
199
  const head = result.stdout.trim();
141
200
  return head.length > 0 ? head : null;
142
201
  }
@@ -0,0 +1,56 @@
1
+ /** Public component health states derived from doctor checks. */
2
+
3
+ export const COMPONENT_HEALTH = Object.freeze({
4
+ HEALTHY: "healthy",
5
+ DEGRADED: "degraded",
6
+ DRIFTED: "drifted",
7
+ MISSING: "missing"
8
+ });
9
+
10
+ const CHECK_TO_HEALTH = Object.freeze({
11
+ missing: COMPONENT_HEALTH.MISSING,
12
+ stale: COMPONENT_HEALTH.DRIFTED,
13
+ warning: COMPONENT_HEALTH.DEGRADED,
14
+ ok: COMPONENT_HEALTH.HEALTHY
15
+ });
16
+
17
+ const HEALTH_RANK = Object.freeze({
18
+ [COMPONENT_HEALTH.MISSING]: 3,
19
+ [COMPONENT_HEALTH.DRIFTED]: 2,
20
+ [COMPONENT_HEALTH.DEGRADED]: 1,
21
+ [COMPONENT_HEALTH.HEALTHY]: 0
22
+ });
23
+
24
+ export function summarizeComponentHealth(checks = []) {
25
+ if (!Array.isArray(checks) || checks.length === 0) {
26
+ return COMPONENT_HEALTH.HEALTHY;
27
+ }
28
+
29
+ let worst = COMPONENT_HEALTH.HEALTHY;
30
+ for (const check of checks) {
31
+ const mapped = CHECK_TO_HEALTH[check.status] ?? COMPONENT_HEALTH.DEGRADED;
32
+ if (HEALTH_RANK[mapped] > HEALTH_RANK[worst]) worst = mapped;
33
+ }
34
+ return worst;
35
+ }
36
+
37
+ export function buildComponentHealthEntries(components, checks) {
38
+ return components.map((component) => {
39
+ const related = checks.filter((check) => check.componentId === component.id);
40
+ return {
41
+ id: component.id,
42
+ version: component.version,
43
+ source: component.source ?? "bundled",
44
+ status: summarizeComponentHealth(related),
45
+ checks: related.map((check) => ({
46
+ name: check.name,
47
+ status: check.status,
48
+ type: check.category ?? null
49
+ }))
50
+ };
51
+ });
52
+ }
53
+
54
+ export function doctorAffectedByComponentHealth(health) {
55
+ return health === COMPONENT_HEALTH.MISSING || health === COMPONENT_HEALTH.DRIFTED;
56
+ }