@kal-elsam/kairo-runtime 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",
package/src/cli.js CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  import { applyPolicyToOptions, loadPolicyFile } from "./global/policy.js";
31
31
  import { resolveHomeDir } from "./global/paths.js";
32
32
  import { runWorkspaceDetect, runWorkspaceDoctor, runWorkspaceInit, runWorkspaceUpdate } from "./workspace-cli.js";
33
+ import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orchestrator.js";
33
34
  import {
34
35
  LEGACY_PACKAGE_NAME,
35
36
  PACKAGE_NAME,
@@ -66,6 +67,24 @@ export async function runCli(argv) {
66
67
  const invoke = resolveSuggestedInvocation(packageManifest.name);
67
68
 
68
69
  switch (command) {
70
+ case "shell":
71
+ await runOrchestratorShell({
72
+ packageRoot,
73
+ packageManifest,
74
+ workspaceRoot: optionsWithPolicy.cwd,
75
+ interactive: optionsWithPolicy.interactive
76
+ });
77
+ return;
78
+ case "orchestrator":
79
+ await runOrchestratorDiagnostics({
80
+ homeDir: resolveHomeDir(),
81
+ workspaceRoot: optionsWithPolicy.cwd,
82
+ packageName: packageManifest.name,
83
+ packageRoot,
84
+ cliVersion: packageManifest.version,
85
+ json: optionsWithPolicy.json
86
+ });
87
+ return;
69
88
  case "setup":
70
89
  await runGlobalSetup(optionsWithPolicy, packageManifest, packageRoot);
71
90
  return;
@@ -219,7 +238,37 @@ function resolveImplicitCommand(args) {
219
238
  if (argsWantsWorkspaceScope(args)) {
220
239
  return "init";
221
240
  }
222
- return "setup";
241
+ if (hasImplicitSetupFlags(args)) {
242
+ return "setup";
243
+ }
244
+ return "shell";
245
+ }
246
+
247
+ function hasImplicitSetupFlags(args) {
248
+ const setupFlags = new Set([
249
+ "--dry-run",
250
+ "--yes",
251
+ "-y",
252
+ "--confirm",
253
+ "--simple",
254
+ "--no-preflight",
255
+ "--all-adapters",
256
+ "--no-default-components",
257
+ "--detect"
258
+ ]);
259
+
260
+ for (let index = 0; index < args.length; index += 1) {
261
+ const arg = args[index];
262
+ if (setupFlags.has(arg)) return true;
263
+ if (arg.startsWith("--mode=")) return true;
264
+ if (arg.startsWith("--adapters=") || arg.startsWith("--agents=")) return true;
265
+ if (arg.startsWith("--components=")) return true;
266
+ if (arg === "--mode" || arg === "--adapters" || arg === "--agents" || arg === "--components") {
267
+ return true;
268
+ }
269
+ }
270
+
271
+ return false;
223
272
  }
224
273
 
225
274
  function argsWantsWorkspaceScope(args) {
@@ -461,6 +510,8 @@ function normalizeCommand(command) {
461
510
  if (!command) return "install";
462
511
 
463
512
  if (command === "install" || command === "i") return "install";
513
+ if (command === "shell") return "shell";
514
+ if (command === "orchestrator") return "orchestrator";
464
515
  if (command === "setup") return "setup";
465
516
  if (command === "status") return "status";
466
517
  if (command === "sync") return "sync";
@@ -517,8 +568,11 @@ sections, components, backups, and drift repair under ~/.harness.
517
568
  Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
518
569
 
519
570
  Usage:
520
- ${cli} [--dry-run] [--yes] [--confirm] [--agents <list|all>] [--components <list>]
571
+ ${cli} Interactive orchestrator shell (TTY)
572
+ ${cli} --dry-run Setup dry-run (scriptable)
521
573
  ${cli} --version
574
+ ${cli} shell Interactive orchestrator shell (TTY)
575
+ ${cli} orchestrator [--json] Read-only agent capability diagnostics
522
576
  ${cli} setup [--dry-run] [--yes] [--confirm] [--simple] [--no-preflight] [--agents <list|all>] [--components <list>]
523
577
  ${cli} status [--json]
524
578
  ${cli} sync [--dry-run] [--yes] [--confirm] [--json] [--no-preflight]
@@ -552,7 +606,9 @@ Scopes:
552
606
  Explicit --scope=workspace only.
553
607
 
554
608
  Commands:
555
- setup Interactive Ink UI (TTY). Use --simple for Clack prompts.
609
+ shell Interactive Ink orchestrator (TTY). Bare ${cli} opens this in TTY sessions.
610
+ orchestrator Read-only capability registry diagnostics (--json supported).
611
+ setup Managed ecosystem setup. Interactive Ink UI (TTY). Use --simple for Clack prompts.
556
612
  status Control panel: agents, components, drift, backups, next action.
557
613
  sync Converge managed content (repair drift), then show status.
558
614
  upgrade Preview or apply ecosystem updates (apply requires --yes).
@@ -0,0 +1,178 @@
1
+ import { CAPABILITY_STATES } from "./capability-states.js";
2
+ import { inspectAllCapabilities } from "./capability-registry.js";
3
+ import { resolveProfile, resolveProfileAgents } from "./profile.js";
4
+ import { formatCliCommand } from "./brand/cli.js";
5
+
6
+ export const PLAN_ACTIONS = {
7
+ DIAGNOSE: "diagnose",
8
+ SETUP: "setup",
9
+ SYNC: "sync",
10
+ INSTALL: "install",
11
+ STATUS: "status"
12
+ };
13
+
14
+ export async function buildReadOnlyDiagnostics({
15
+ homeDir,
16
+ workspaceRoot,
17
+ packageName,
18
+ packageRoot,
19
+ cliVersion
20
+ }) {
21
+ const [{ profile, sources }, capabilities] = await Promise.all([
22
+ resolveProfile({ homeDir, workspaceRoot }),
23
+ inspectAllCapabilities({ homeDir, workspaceRoot, packageName })
24
+ ]);
25
+
26
+ const detectedIds = capabilities.filter((entry) => entry.detected).map((entry) => entry.id);
27
+ const profileAgents = resolveProfileAgents(profile, detectedIds);
28
+ const diagnostics = summarizeDiagnostics(capabilities);
29
+
30
+ return {
31
+ readOnly: true,
32
+ cliVersion,
33
+ profile: { ...profile, sources },
34
+ capabilities,
35
+ profileAgents,
36
+ diagnostics,
37
+ recommendations: buildDiagnosticRecommendations({ capabilities, profile, diagnostics })
38
+ };
39
+ }
40
+
41
+ export async function buildActionPlan({
42
+ action,
43
+ homeDir,
44
+ workspaceRoot,
45
+ packageName,
46
+ options = {}
47
+ }) {
48
+ const diagnostics = await buildReadOnlyDiagnostics({
49
+ homeDir,
50
+ workspaceRoot,
51
+ packageName,
52
+ packageRoot: options.packageRoot ?? null,
53
+ cliVersion: options.cliVersion ?? null
54
+ });
55
+
56
+ const steps = [];
57
+ const warnings = [];
58
+
59
+ switch (action) {
60
+ case PLAN_ACTIONS.DIAGNOSE:
61
+ case PLAN_ACTIONS.STATUS:
62
+ return {
63
+ action,
64
+ readOnly: true,
65
+ requiresConfirmation: false,
66
+ steps: ["Show ecosystem diagnostics (read-only)."],
67
+ diagnostics,
68
+ warnings
69
+ };
70
+ case PLAN_ACTIONS.SETUP:
71
+ case PLAN_ACTIONS.INSTALL:
72
+ steps.push(`Target agents: ${diagnostics.profileAgents.join(", ")}`);
73
+ steps.push("Preview managed section changes under ~/.harness and agent config files.");
74
+ steps.push("Create backups before writing managed content.");
75
+ if (options.dryRun) {
76
+ steps.push("Dry run: no files will be written.");
77
+ }
78
+ return {
79
+ action,
80
+ readOnly: Boolean(options.dryRun),
81
+ requiresConfirmation: !options.dryRun,
82
+ steps,
83
+ diagnostics,
84
+ warnings: collectCapabilityWarnings(diagnostics.capabilities)
85
+ };
86
+ case PLAN_ACTIONS.SYNC:
87
+ steps.push("Compare managed content against bundled assets.");
88
+ steps.push("Repair drift in agent config files when differences are found.");
89
+ if (options.dryRun) {
90
+ steps.push("Dry run: show planned repairs only.");
91
+ }
92
+ return {
93
+ action,
94
+ readOnly: Boolean(options.dryRun),
95
+ requiresConfirmation: !options.dryRun,
96
+ steps,
97
+ diagnostics,
98
+ warnings: collectCapabilityWarnings(diagnostics.capabilities)
99
+ };
100
+ default: {
101
+ const _exhaustive = action;
102
+ throw new Error(`Unknown plan action "${_exhaustive}".`);
103
+ }
104
+ }
105
+ }
106
+
107
+ export function shouldExecutePlan(plan, { confirmed = false } = {}) {
108
+ if (plan.readOnly) return true;
109
+ return confirmed;
110
+ }
111
+
112
+ export function formatActionPlan(plan) {
113
+ const lines = [];
114
+ lines.push(`Action: ${plan.action}`);
115
+ lines.push(`Mode: ${plan.readOnly ? "read-only" : "write"}`);
116
+ lines.push(`Confirmation required: ${plan.requiresConfirmation ? "yes" : "no"}`);
117
+ lines.push("");
118
+ lines.push("Steps:");
119
+ for (const step of plan.steps) {
120
+ lines.push(` - ${step}`);
121
+ }
122
+
123
+ if (plan.warnings.length > 0) {
124
+ lines.push("");
125
+ lines.push("Warnings:");
126
+ for (const warning of plan.warnings) {
127
+ lines.push(` - ${warning}`);
128
+ }
129
+ }
130
+
131
+ if (plan.diagnostics.recommendations.length > 0) {
132
+ lines.push("");
133
+ lines.push("Recommendations:");
134
+ for (const recommendation of plan.diagnostics.recommendations) {
135
+ lines.push(` - ${recommendation}`);
136
+ }
137
+ }
138
+
139
+ return lines.join("\n");
140
+ }
141
+
142
+ function summarizeDiagnostics(capabilities) {
143
+ return {
144
+ detected: capabilities.filter((entry) => entry.detected).length,
145
+ available: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.AVAILABLE).length,
146
+ unknown: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.UNKNOWN).length,
147
+ errors: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.ERROR).length
148
+ };
149
+ }
150
+
151
+ function buildDiagnosticRecommendations({ capabilities, profile, diagnostics }) {
152
+ const recommendations = [];
153
+
154
+ if (diagnostics.detected === 0) {
155
+ recommendations.push(`No agents detected. Run ${formatCliCommand("detect")} or install a supported agent CLI.`);
156
+ }
157
+
158
+ if (profile.coordinator) {
159
+ const coordinator = capabilities.find((entry) => entry.id === profile.coordinator);
160
+ if (!coordinator?.detected && coordinator?.state === CAPABILITY_STATES.UNKNOWN) {
161
+ recommendations.push(`Profile coordinator "${profile.coordinator}" is not detected on this machine.`);
162
+ }
163
+ }
164
+
165
+ for (const capability of capabilities) {
166
+ if (capability.recommendation) {
167
+ recommendations.push(`${capability.label}: ${capability.recommendation}`);
168
+ }
169
+ }
170
+
171
+ return [...new Set(recommendations)];
172
+ }
173
+
174
+ function collectCapabilityWarnings(capabilities) {
175
+ return capabilities
176
+ .filter((entry) => entry.state === CAPABILITY_STATES.ERROR || entry.state === CAPABILITY_STATES.UNKNOWN)
177
+ .map((entry) => `${entry.label} is ${entry.state}${entry.error ? ` (${entry.error})` : ""}.`);
178
+ }
@@ -0,0 +1,224 @@
1
+ import { CAPABILITY_STATES } from "../capability-states.js";
2
+ import {
3
+ isExecutableAvailable,
4
+ parseVersionFromOutput,
5
+ probeCommand,
6
+ resolveProbeState
7
+ } from "../cli-probe.js";
8
+
9
+ export function createAgentCapabilityAdapter({
10
+ id,
11
+ label,
12
+ managedAdapter,
13
+ executable = null,
14
+ versionArgs = ["--version"],
15
+ authArgs = null,
16
+ modelsArgs = null,
17
+ opaqueAuth = false,
18
+ runExecutable = null
19
+ }) {
20
+ const cliName = runExecutable ?? executable;
21
+
22
+ return {
23
+ id,
24
+ label,
25
+ managedAdapter,
26
+
27
+ detect(context) {
28
+ return managedAdapter.detect(context);
29
+ },
30
+
31
+ inspect(context, { probeImpl = defaultProbe } = {}) {
32
+ const detected = managedAdapter.detect(context);
33
+ const cliAvailable = executable ? isExecutableAvailable(executable) : false;
34
+
35
+ if (!detected && !cliAvailable) {
36
+ return buildInspection({
37
+ id,
38
+ label,
39
+ state: CAPABILITY_STATES.UNKNOWN,
40
+ detected: false,
41
+ cliAvailable: false,
42
+ version: null,
43
+ authenticated: null,
44
+ recommendation: `Install ${label} or run ${formatCliCommand("setup")} to configure managed sections.`
45
+ });
46
+ }
47
+
48
+ if (opaqueAuth) {
49
+ return buildInspection({
50
+ id,
51
+ label,
52
+ state: detected || cliAvailable ? CAPABILITY_STATES.DETECTED : CAPABILITY_STATES.UNKNOWN,
53
+ detected,
54
+ cliAvailable,
55
+ version: null,
56
+ authenticated: null,
57
+ recommendation: detected
58
+ ? `${label} config detected. Authentication status is provider-managed.`
59
+ : `Install ${label} to enable managed configuration.`
60
+ });
61
+ }
62
+
63
+ return probeImpl({
64
+ id,
65
+ label,
66
+ detected,
67
+ cliAvailable,
68
+ executable,
69
+ versionArgs,
70
+ authArgs
71
+ });
72
+ },
73
+
74
+ listModels(context, { probeImpl = defaultProbeModels } = {}) {
75
+ if (!modelsArgs || !executable) return null;
76
+ if (!isExecutableAvailable(executable)) return null;
77
+ return probeImpl({ executable, modelsArgs, context });
78
+ },
79
+
80
+ run(context, { args = [], cwd = context.workspaceRoot ?? process.cwd(), spawnImpl = probeCommand } = {}) {
81
+ if (!cliName) {
82
+ return {
83
+ ok: false,
84
+ state: CAPABILITY_STATES.ERROR,
85
+ message: `${label} does not expose a delegatable CLI through Kairo.`
86
+ };
87
+ }
88
+
89
+ if (!isExecutableAvailable(cliName)) {
90
+ return {
91
+ ok: false,
92
+ state: CAPABILITY_STATES.ERROR,
93
+ message: `${label} CLI "${cliName}" is not on PATH. Install the agent or add it to PATH.`
94
+ };
95
+ }
96
+
97
+ const result = spawnImpl(cliName, args, { cwd, env: process.env });
98
+
99
+ if (!result.ok) {
100
+ const detail = result.stderr || result.stdout || result.error || "unknown error";
101
+ return {
102
+ ok: false,
103
+ state: CAPABILITY_STATES.ERROR,
104
+ message: `${label} CLI failed: ${detail}`
105
+ };
106
+ }
107
+
108
+ return {
109
+ ok: true,
110
+ state: CAPABILITY_STATES.AVAILABLE,
111
+ stdout: result.stdout,
112
+ stderr: result.stderr
113
+ };
114
+ }
115
+ };
116
+ }
117
+
118
+ function defaultProbe({ id, label, detected, cliAvailable, executable, versionArgs, authArgs }) {
119
+ let version = null;
120
+ let authenticated = null;
121
+ let probeError = null;
122
+ let authReady = false;
123
+
124
+ if (cliAvailable && executable) {
125
+ const versionResult = probeCommand(executable, versionArgs);
126
+ if (versionResult.timedOut || versionResult.error) {
127
+ probeError = versionResult.error ?? "probe timed out";
128
+ } else if (versionResult.ok) {
129
+ version = parseVersionFromOutput(versionResult.stdout) ?? versionResult.stdout.split("\n")[0] ?? null;
130
+ }
131
+
132
+ if (authArgs) {
133
+ const authResult = probeCommand(executable, authArgs);
134
+ if (authResult.timedOut || authResult.error) {
135
+ probeError = probeError ?? authResult.error ?? "auth probe timed out";
136
+ } else {
137
+ authenticated = authResult.ok;
138
+ authReady = authResult.ok;
139
+ }
140
+ } else if (version) {
141
+ authenticated = null;
142
+ authReady = true;
143
+ }
144
+ }
145
+
146
+ const state = resolveProbeState({
147
+ detected,
148
+ cliAvailable,
149
+ authReady,
150
+ probeError,
151
+ opaque: false
152
+ });
153
+
154
+ return buildInspection({
155
+ id,
156
+ label,
157
+ state,
158
+ detected,
159
+ cliAvailable,
160
+ version,
161
+ authenticated,
162
+ error: probeError,
163
+ recommendation: buildRecommendation({ label, state, detected, cliAvailable, authenticated })
164
+ });
165
+ }
166
+
167
+ function defaultProbeModels({ executable, modelsArgs }) {
168
+ const result = probeCommand(executable, modelsArgs);
169
+ if (!result.ok) return null;
170
+
171
+ const lines = result.stdout
172
+ .split("\n")
173
+ .map((line) => line.trim())
174
+ .filter(Boolean);
175
+
176
+ return lines.length > 0 ? lines : null;
177
+ }
178
+
179
+ function buildInspection(fields) {
180
+ return {
181
+ id: fields.id,
182
+ label: fields.label,
183
+ state: fields.state,
184
+ detected: fields.detected,
185
+ cliAvailable: fields.cliAvailable,
186
+ version: fields.version ?? null,
187
+ authenticated: fields.authenticated ?? null,
188
+ models: fields.models ?? null,
189
+ error: fields.error ?? null,
190
+ recommendation: fields.recommendation ?? null
191
+ };
192
+ }
193
+
194
+ function buildRecommendation({ label, state, detected, cliAvailable, authenticated }) {
195
+ if (state === CAPABILITY_STATES.ERROR) {
196
+ return `Re-run detection or check ${label} CLI logs for details.`;
197
+ }
198
+
199
+ if (state === CAPABILITY_STATES.UNKNOWN) {
200
+ return `Install ${label} or run ${formatCliCommand("detect")} after setup.`;
201
+ }
202
+
203
+ if (state === CAPABILITY_STATES.AVAILABLE) {
204
+ return `${label} is ready. Delegate tasks through its CLI.`;
205
+ }
206
+
207
+ if (authenticated === false) {
208
+ return `Authenticate with ${label} (provider login) before delegating work.`;
209
+ }
210
+
211
+ if (detected && !cliAvailable) {
212
+ return `${label} config detected but CLI not on PATH.`;
213
+ }
214
+
215
+ if (cliAvailable && !detected) {
216
+ return `${label} CLI found. Run ${formatCliCommand("setup")} to add managed sections.`;
217
+ }
218
+
219
+ return `${label} detected. Run ${formatCliCommand("status")} for ecosystem health.`;
220
+ }
221
+
222
+ function formatCliCommand(command) {
223
+ return `kairo ${command}`;
224
+ }
@@ -0,0 +1,54 @@
1
+ import cursorManaged from "../adapters/cursor.js";
2
+ import codexManaged from "../adapters/codex.js";
3
+ import opencodeManaged from "../adapters/opencode.js";
4
+ import claudeManaged from "../adapters/claude.js";
5
+ import { createAgentCapabilityAdapter } from "./create-capability-adapter.js";
6
+
7
+ const cursor = createAgentCapabilityAdapter({
8
+ id: "cursor",
9
+ label: "Cursor",
10
+ managedAdapter: cursorManaged,
11
+ executable: "cursor",
12
+ opaqueAuth: true
13
+ });
14
+
15
+ const codex = createAgentCapabilityAdapter({
16
+ id: "codex",
17
+ label: "Codex",
18
+ managedAdapter: codexManaged,
19
+ executable: "codex",
20
+ runExecutable: "codex"
21
+ });
22
+
23
+ const opencode = createAgentCapabilityAdapter({
24
+ id: "opencode",
25
+ label: "OpenCode",
26
+ managedAdapter: opencodeManaged,
27
+ executable: "opencode",
28
+ runExecutable: "opencode"
29
+ });
30
+
31
+ const claude = createAgentCapabilityAdapter({
32
+ id: "claude",
33
+ label: "Claude Code",
34
+ managedAdapter: claudeManaged,
35
+ executable: "claude",
36
+ authArgs: ["auth", "status"],
37
+ runExecutable: "claude"
38
+ });
39
+
40
+ const CAPABILITY_ADAPTERS = [cursor, codex, opencode, claude];
41
+
42
+ export const AGENT_CAPABILITY_IDS = CAPABILITY_ADAPTERS.map((adapter) => adapter.id);
43
+
44
+ export function listCapabilityAdapters() {
45
+ return [...CAPABILITY_ADAPTERS];
46
+ }
47
+
48
+ export function resolveCapabilityAdapter(id) {
49
+ const adapter = CAPABILITY_ADAPTERS.find((candidate) => candidate.id === id);
50
+ if (!adapter) {
51
+ throw new Error(`Unknown agent capability "${id}". Use ${AGENT_CAPABILITY_IDS.join(", ")}.`);
52
+ }
53
+ return adapter;
54
+ }
@@ -51,9 +51,9 @@ export function formatCliCommand(subcommand, cliName = PREFERRED_CLI) {
51
51
  return trimmed ? `${cliName} ${trimmed}` : cliName;
52
52
  }
53
53
 
54
- function detectInvocationPackageManager() {
55
- const execPath = process.env.npm_execpath ?? "";
56
- const userAgent = process.env.npm_config_user_agent ?? "";
54
+ function detectInvocationPackageManager(env = process.env) {
55
+ const execPath = env.npm_execpath ?? "";
56
+ const userAgent = env.npm_config_user_agent ?? "";
57
57
 
58
58
  if (execPath.includes("pnpm") || userAgent.startsWith("pnpm/")) return "pnpm";
59
59
  if (execPath.includes("yarn") || userAgent.startsWith("yarn/")) return "yarn";
@@ -61,7 +61,11 @@ function detectInvocationPackageManager() {
61
61
  return "npm";
62
62
  }
63
63
 
64
- export function resolveSuggestedInvocation(packageName = PACKAGE_NAME, argv = process.argv) {
64
+ export function resolveSuggestedInvocation(
65
+ packageName = PACKAGE_NAME,
66
+ argv = process.argv,
67
+ { env = process.env } = {}
68
+ ) {
65
69
  const invokedPath = argv[1] ?? PREFERRED_CLI;
66
70
  const invokedBase = basename(invokedPath);
67
71
 
@@ -69,7 +73,7 @@ export function resolveSuggestedInvocation(packageName = PACKAGE_NAME, argv = pr
69
73
  return invokedBase;
70
74
  }
71
75
 
72
- const packageManager = detectInvocationPackageManager();
76
+ const packageManager = detectInvocationPackageManager(env);
73
77
 
74
78
  switch (packageManager) {
75
79
  case "pnpm":
@@ -85,9 +89,9 @@ export function resolveSuggestedInvocation(packageName = PACKAGE_NAME, argv = pr
85
89
 
86
90
  export function formatSuggestedCliCommand(
87
91
  subcommand,
88
- { packageName = PACKAGE_NAME, argv = process.argv, suggestedInvocation } = {}
92
+ { packageName = PACKAGE_NAME, argv = process.argv, suggestedInvocation, env = process.env } = {}
89
93
  ) {
90
- const invoke = suggestedInvocation ?? resolveSuggestedInvocation(packageName, argv);
94
+ const invoke = suggestedInvocation ?? resolveSuggestedInvocation(packageName, argv, { env });
91
95
  const trimmed = subcommand.trim();
92
96
  return trimmed ? `${invoke} ${trimmed}` : invoke;
93
97
  }
@@ -0,0 +1,80 @@
1
+ import { buildAdapterContext } from "./adapter-context.js";
2
+ import { CAPABILITY_STATES } from "./capability-states.js";
3
+ import {
4
+ AGENT_CAPABILITY_IDS,
5
+ listCapabilityAdapters,
6
+ resolveCapabilityAdapter
7
+ } from "./agent-capabilities/index.js";
8
+
9
+ export async function inspectAllCapabilities({
10
+ homeDir,
11
+ workspaceRoot = process.cwd(),
12
+ packageName = "@kal-elsam/kairo-runtime",
13
+ probeOverrides = null
14
+ } = {}) {
15
+ const context = buildAdapterContext({ homeDir, workspaceRoot, packageName });
16
+ const adapters = listCapabilityAdapters();
17
+
18
+ return Promise.all(adapters.map(async (adapter) => {
19
+ const probeOptions = probeOverrides?.[adapter.id] ?? {};
20
+ const inspection = adapter.inspect(context, probeOptions);
21
+ const models = adapter.listModels?.(context, probeOptions) ?? null;
22
+
23
+ return {
24
+ ...inspection,
25
+ models
26
+ };
27
+ }));
28
+ }
29
+
30
+ export async function inspectCapability(agentId, context, probeOptions = {}) {
31
+ const adapter = resolveCapabilityAdapter(agentId);
32
+ const inspection = adapter.inspect(context, probeOptions);
33
+ const models = adapter.listModels?.(context, probeOptions) ?? null;
34
+ return { ...inspection, models };
35
+ }
36
+
37
+ export function summarizeCapabilityRegistry(capabilities) {
38
+ const byState = {};
39
+ for (const state of Object.values(CAPABILITY_STATES)) {
40
+ byState[state] = 0;
41
+ }
42
+
43
+ for (const capability of capabilities) {
44
+ if (byState[capability.state] != null) {
45
+ byState[capability.state] += 1;
46
+ }
47
+ }
48
+
49
+ return {
50
+ total: capabilities.length,
51
+ supported: AGENT_CAPABILITY_IDS.length,
52
+ detected: capabilities.filter((entry) => entry.detected).length,
53
+ available: capabilities.filter((entry) => entry.state === CAPABILITY_STATES.AVAILABLE).length,
54
+ byState
55
+ };
56
+ }
57
+
58
+ export function buildCapabilityDiagnostics(capabilities) {
59
+ const recommendations = capabilities
60
+ .map((entry) => entry.recommendation)
61
+ .filter(Boolean);
62
+
63
+ const errors = capabilities
64
+ .filter((entry) => entry.state === CAPABILITY_STATES.ERROR)
65
+ .map((entry) => ({
66
+ agent: entry.id,
67
+ message: entry.error ?? entry.recommendation ?? "Inspection failed."
68
+ }));
69
+
70
+ return {
71
+ recommendations,
72
+ errors,
73
+ hasActionableErrors: errors.length > 0
74
+ };
75
+ }
76
+
77
+ export function delegateToAgent(agentId, context, runOptions = {}) {
78
+ const adapter = resolveCapabilityAdapter(agentId);
79
+ return adapter.run(context, runOptions);
80
+ }
@@ -0,0 +1,30 @@
1
+ export const CAPABILITY_STATES = {
2
+ DETECTED: "detected",
3
+ AUTHENTICATED: "authenticated",
4
+ AVAILABLE: "available",
5
+ UNKNOWN: "unknown",
6
+ ERROR: "error"
7
+ };
8
+
9
+ export function isCapabilityState(value) {
10
+ return Object.values(CAPABILITY_STATES).includes(value);
11
+ }
12
+
13
+ export function formatCapabilityState(state) {
14
+ switch (state) {
15
+ case CAPABILITY_STATES.DETECTED:
16
+ return "detected";
17
+ case CAPABILITY_STATES.AUTHENTICATED:
18
+ return "authenticated";
19
+ case CAPABILITY_STATES.AVAILABLE:
20
+ return "available";
21
+ case CAPABILITY_STATES.UNKNOWN:
22
+ return "unknown";
23
+ case CAPABILITY_STATES.ERROR:
24
+ return "error";
25
+ default: {
26
+ const _exhaustive = state;
27
+ return String(_exhaustive);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,47 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ const DEFAULT_TIMEOUT_MS = 5000;
4
+
5
+ export function isExecutableAvailable(command, { env = process.env } = {}) {
6
+ const result = spawnSync("which", [command], { encoding: "utf8", env });
7
+ return result.status === 0 && result.stdout.trim().length > 0;
8
+ }
9
+
10
+ export function probeCommand(command, args = [], {
11
+ cwd = process.cwd(),
12
+ env = process.env,
13
+ timeoutMs = DEFAULT_TIMEOUT_MS
14
+ } = {}) {
15
+ const result = spawnSync(command, args, {
16
+ cwd,
17
+ env,
18
+ encoding: "utf8",
19
+ timeout: timeoutMs
20
+ });
21
+
22
+ return {
23
+ ok: result.status === 0,
24
+ status: result.status,
25
+ stdout: (result.stdout ?? "").trim(),
26
+ stderr: (result.stderr ?? "").trim(),
27
+ error: result.error?.message ?? null,
28
+ timedOut: result.error?.code === "ETIMEDOUT"
29
+ };
30
+ }
31
+
32
+ export function parseVersionFromOutput(output) {
33
+ if (!output) return null;
34
+
35
+ const match = output.match(/(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
36
+ return match?.[1] ?? null;
37
+ }
38
+
39
+ export function resolveProbeState({ detected, cliAvailable, authReady, probeError, opaque = false }) {
40
+ if (probeError) return "error";
41
+ if (opaque) return "unknown";
42
+ if (!detected && !cliAvailable) return "unknown";
43
+ if (cliAvailable && authReady) return "available";
44
+ if (authReady) return "authenticated";
45
+ if (detected || cliAvailable) return "detected";
46
+ return "unknown";
47
+ }
@@ -0,0 +1,209 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Box, Text, useApp, useInput } from "ink";
3
+ import { BRAND } from "../brand/index.js";
4
+ import { PLAN_ACTIONS, buildActionPlan, buildReadOnlyDiagnostics } from "../action-planner.js";
5
+ import { buildProfileJson, resolveProfile } from "../profile.js";
6
+ import {
7
+ ORCHESTRATOR_MENU,
8
+ ORCHESTRATOR_VIEWS,
9
+ formatAgentStatusLines,
10
+ formatPlanLines,
11
+ formatProfileLines
12
+ } from "./orchestrator-state.js";
13
+
14
+ const COLORS = {
15
+ accent: "cyan",
16
+ success: "green",
17
+ warning: "yellow",
18
+ danger: "red",
19
+ muted: "gray"
20
+ };
21
+
22
+ export function OrchestratorApp({
23
+ homeDir,
24
+ workspaceRoot,
25
+ packageName,
26
+ packageRoot,
27
+ cliVersion,
28
+ onComplete
29
+ }) {
30
+ const { exit } = useApp();
31
+ const [view, setView] = useState(ORCHESTRATOR_VIEWS.HOME);
32
+ const [menuIndex, setMenuIndex] = useState(0);
33
+ const [loading, setLoading] = useState(true);
34
+ const [error, setError] = useState(null);
35
+ const [diagnostics, setDiagnostics] = useState(null);
36
+ const [profileJson, setProfileJson] = useState(null);
37
+ const [plan, setPlan] = useState(null);
38
+
39
+ useEffect(() => {
40
+ let cancelled = false;
41
+
42
+ async function load() {
43
+ try {
44
+ const [diag, profileResolved] = await Promise.all([
45
+ buildReadOnlyDiagnostics({ homeDir, workspaceRoot, packageName, packageRoot, cliVersion }),
46
+ resolveProfile({ homeDir, workspaceRoot })
47
+ ]);
48
+
49
+ if (cancelled) return;
50
+ setDiagnostics(diag);
51
+ setProfileJson(buildProfileJson(profileResolved));
52
+ setLoading(false);
53
+ } catch (loadError) {
54
+ if (cancelled) return;
55
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
56
+ setLoading(false);
57
+ }
58
+ }
59
+
60
+ load();
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ }, [homeDir, workspaceRoot, packageName, packageRoot, cliVersion]);
65
+
66
+ const finish = (outcome) => {
67
+ onComplete(outcome);
68
+ exit();
69
+ };
70
+
71
+ useInput((inputKey, key) => {
72
+ if (key.escape) {
73
+ if (view === ORCHESTRATOR_VIEWS.HOME) {
74
+ finish({ cancelled: true });
75
+ return;
76
+ }
77
+ setView(ORCHESTRATOR_VIEWS.HOME);
78
+ setPlan(null);
79
+ return;
80
+ }
81
+
82
+ if (loading || error) return;
83
+
84
+ if (view === ORCHESTRATOR_VIEWS.CONFIRM) {
85
+ if (inputKey.toLowerCase() === "y") {
86
+ finish({ cancelled: false, action: plan?.action ?? PLAN_ACTIONS.SETUP, confirmed: true, plan });
87
+ }
88
+ if (inputKey.toLowerCase() === "n") {
89
+ setView(ORCHESTRATOR_VIEWS.HOME);
90
+ setPlan(null);
91
+ }
92
+ return;
93
+ }
94
+
95
+ if (view !== ORCHESTRATOR_VIEWS.HOME) return;
96
+
97
+ if (key.upArrow) {
98
+ setMenuIndex((index) => Math.max(0, index - 1));
99
+ return;
100
+ }
101
+
102
+ if (key.downArrow) {
103
+ setMenuIndex((index) => Math.min(ORCHESTRATOR_MENU.length - 1, index + 1));
104
+ return;
105
+ }
106
+
107
+ if (!key.return) return;
108
+
109
+ const item = ORCHESTRATOR_MENU[menuIndex];
110
+ if (item.action === "setup") {
111
+ buildActionPlan({
112
+ action: PLAN_ACTIONS.SETUP,
113
+ homeDir,
114
+ workspaceRoot,
115
+ packageName,
116
+ options: { packageRoot, cliVersion }
117
+ }).then((builtPlan) => {
118
+ setPlan(builtPlan);
119
+ setView(ORCHESTRATOR_VIEWS.CONFIRM);
120
+ }).catch((planError) => {
121
+ setError(planError instanceof Error ? planError.message : String(planError));
122
+ });
123
+ return;
124
+ }
125
+
126
+ setView(item.view);
127
+ });
128
+
129
+ if (loading) {
130
+ return React.createElement(Box, { flexDirection: "column" },
131
+ React.createElement(Text, { bold: true, color: COLORS.accent }, BRAND.displayName),
132
+ React.createElement(Text, { color: COLORS.muted }, "Loading agent capabilities…")
133
+ );
134
+ }
135
+
136
+ if (error) {
137
+ return React.createElement(Box, { flexDirection: "column" },
138
+ React.createElement(Text, { bold: true, color: COLORS.danger }, "Orchestrator error"),
139
+ React.createElement(Text, null, error),
140
+ React.createElement(Text, { dimColor: true }, "Esc to exit")
141
+ );
142
+ }
143
+
144
+ return React.createElement(Box, { flexDirection: "column" },
145
+ React.createElement(Text, { bold: true, color: COLORS.accent }, `${BRAND.displayName} orchestrator`),
146
+ React.createElement(Text, { color: COLORS.muted }, "Provider-neutral agent coordinator · no model imposed"),
147
+ React.createElement(Text, null, ""),
148
+ renderView({ view, diagnostics, profileJson, plan, menuIndex }),
149
+ React.createElement(Text, null, ""),
150
+ React.createElement(Text, { dimColor: true }, footerHint(view))
151
+ );
152
+ }
153
+
154
+ function renderView({ view, diagnostics, profileJson, plan, menuIndex }) {
155
+ switch (view) {
156
+ case ORCHESTRATOR_VIEWS.HOME:
157
+ return React.createElement(Box, { flexDirection: "column" },
158
+ React.createElement(Text, { bold: true }, "Menu"),
159
+ ORCHESTRATOR_MENU.map((item, index) =>
160
+ React.createElement(Text, {
161
+ key: item.id,
162
+ color: index === menuIndex ? COLORS.accent : undefined,
163
+ bold: index === menuIndex
164
+ }, `${index === menuIndex ? "› " : " "}${item.label}`)
165
+ ),
166
+ React.createElement(Text, null, ""),
167
+ React.createElement(Text, { bold: true }, "Snapshot"),
168
+ React.createElement(Text, null, `Agents detected: ${diagnostics.diagnostics.detected}/${diagnostics.capabilities.length}`),
169
+ React.createElement(Text, null, `Available: ${diagnostics.diagnostics.available}`)
170
+ );
171
+ case ORCHESTRATOR_VIEWS.AGENTS:
172
+ return React.createElement(Box, { flexDirection: "column" },
173
+ React.createElement(Text, { bold: true }, "Agent capabilities"),
174
+ formatAgentStatusLines(diagnostics.capabilities)
175
+ .map((line) => React.createElement(Text, { key: line }, line))
176
+ );
177
+ case ORCHESTRATOR_VIEWS.PROFILE:
178
+ return React.createElement(Box, { flexDirection: "column" },
179
+ React.createElement(Text, { bold: true }, "Profile"),
180
+ formatProfileLines(profileJson)
181
+ .map((line) => React.createElement(Text, { key: line }, line))
182
+ );
183
+ case ORCHESTRATOR_VIEWS.PLAN:
184
+ case ORCHESTRATOR_VIEWS.CONFIRM:
185
+ return React.createElement(Box, { flexDirection: "column" },
186
+ React.createElement(Text, { bold: true }, view === ORCHESTRATOR_VIEWS.CONFIRM ? "Confirm plan" : "Plan"),
187
+ plan && formatPlanLines(plan).map((line) => React.createElement(Text, { key: line }, line)),
188
+ view === ORCHESTRATOR_VIEWS.CONFIRM && React.createElement(Text, { color: COLORS.warning }, "Y confirm · N decline")
189
+ );
190
+ case ORCHESTRATOR_VIEWS.HELP:
191
+ return React.createElement(Box, { flexDirection: "column" },
192
+ React.createElement(Text, { bold: true }, "Help"),
193
+ React.createElement(Text, null, "Kairo coordinates installed agent CLIs — it is not a coding model."),
194
+ React.createElement(Text, null, "Use explicit commands for scripts: setup, install, status, doctor, sync."),
195
+ React.createElement(Text, null, "Profiles: ~/.harness/profile.json and .harness/kairo.json (project wins)."),
196
+ React.createElement(Text, null, "Credentials are never stored by Kairo.")
197
+ );
198
+ default: {
199
+ const _exhaustive = view;
200
+ return React.createElement(Text, null, `Unknown view: ${_exhaustive}`);
201
+ }
202
+ }
203
+ }
204
+
205
+ function footerHint(view) {
206
+ if (view === ORCHESTRATOR_VIEWS.HOME) return "↑↓ navigate · Enter select · Esc quit";
207
+ if (view === ORCHESTRATOR_VIEWS.CONFIRM) return "Y confirm · N decline · Esc back";
208
+ return "Esc back to menu";
209
+ }
@@ -0,0 +1,78 @@
1
+ import { stdin as input, stdout as output } from "node:process";
2
+ import { canUseSetupInk } from "./terminal.js";
3
+
4
+ export function canUseOrchestratorShell({
5
+ interactive = Boolean(input.isTTY && output.isTTY),
6
+ term = process.env.TERM ?? "",
7
+ columns = output.columns ?? 80,
8
+ forceInk = process.env.HARNESS_INK !== "0"
9
+ } = {}) {
10
+ return canUseSetupInk({ interactive, term, columns, forceInk });
11
+ }
12
+
13
+ export const ORCHESTRATOR_VIEWS = {
14
+ HOME: "home",
15
+ AGENTS: "agents",
16
+ PROFILE: "profile",
17
+ PLAN: "plan",
18
+ CONFIRM: "confirm",
19
+ HELP: "help"
20
+ };
21
+
22
+ export const ORCHESTRATOR_MENU = [
23
+ { id: "status", label: "Diagnostics", view: ORCHESTRATOR_VIEWS.HOME },
24
+ { id: "agents", label: "Agents", view: ORCHESTRATOR_VIEWS.AGENTS },
25
+ { id: "profile", label: "Profile", view: ORCHESTRATOR_VIEWS.PROFILE },
26
+ { id: "plan-setup", label: "Plan setup", view: ORCHESTRATOR_VIEWS.PLAN, action: "setup" },
27
+ { id: "help", label: "Help", view: ORCHESTRATOR_VIEWS.HELP }
28
+ ];
29
+
30
+ export function formatAgentStatusLines(capabilities) {
31
+ return capabilities.map((entry) => {
32
+ const auth = entry.authenticated == null ? "n/a" : (entry.authenticated ? "yes" : "no");
33
+ const version = entry.version ?? "unknown";
34
+ return `${entry.label.padEnd(14)} ${entry.state.padEnd(14)} v${version} auth=${auth}`;
35
+ });
36
+ }
37
+
38
+ export function formatProfileLines(profileJson) {
39
+ const lines = [
40
+ `Coordinator: ${profileJson.coordinator ?? "none"}`,
41
+ `Default agents: ${formatAgentsLabel(profileJson.defaultAgents)}`,
42
+ `Apply mode: ${profileJson.applyMode}`
43
+ ];
44
+
45
+ if (profileJson.sources.global) {
46
+ lines.push(`Global: ${profileJson.sources.global}`);
47
+ }
48
+
49
+ if (profileJson.sources.project) {
50
+ lines.push(`Project: ${profileJson.sources.project}`);
51
+ }
52
+
53
+ lines.push(`Precedence: ${profileJson.sources.precedence}`);
54
+ return lines;
55
+ }
56
+
57
+ export function formatPlanLines(plan) {
58
+ const lines = [`Action: ${plan.action}`, ""];
59
+ for (const step of plan.steps) {
60
+ lines.push(` • ${step}`);
61
+ }
62
+
63
+ if (plan.warnings.length > 0) {
64
+ lines.push("", "Warnings:");
65
+ for (const warning of plan.warnings) {
66
+ lines.push(` ! ${warning}`);
67
+ }
68
+ }
69
+
70
+ return lines;
71
+ }
72
+
73
+ function formatAgentsLabel(agents) {
74
+ if (agents === "detected") return "detected";
75
+ if (agents === "all") return "all";
76
+ if (Array.isArray(agents)) return agents.join(", ");
77
+ return String(agents);
78
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { OrchestratorApp } from "./orchestrator-app.js";
4
+
5
+ export async function runOrchestratorInk({
6
+ homeDir,
7
+ workspaceRoot,
8
+ packageRoot,
9
+ packageName,
10
+ cliVersion,
11
+ renderImpl = render
12
+ }) {
13
+ return new Promise((resolve) => {
14
+ const { waitUntilExit } = renderImpl(
15
+ React.createElement(OrchestratorApp, {
16
+ homeDir,
17
+ workspaceRoot,
18
+ packageRoot,
19
+ packageName,
20
+ cliVersion,
21
+ onComplete: resolve
22
+ })
23
+ );
24
+
25
+ waitUntilExit().catch((error) => {
26
+ resolve({ cancelled: true, error });
27
+ });
28
+ });
29
+ }
@@ -0,0 +1,139 @@
1
+ import { stdin as input, stdout as output } from "node:process";
2
+ import { resolveHomeDir } from "./paths.js";
3
+ import { runGlobalSetup } from "./global-cli.js";
4
+ import { PLAN_ACTIONS, buildReadOnlyDiagnostics, shouldExecutePlan } from "./action-planner.js";
5
+ import { canUseOrchestratorShell } from "./ink/orchestrator-state.js";
6
+ import { runOrchestratorInk as defaultRunOrchestratorInk } from "./ink/run-orchestrator-ink.js";
7
+ import { formatCliCommand } from "./brand/cli.js";
8
+ import { BRAND } from "./brand/index.js";
9
+
10
+ export { canUseOrchestratorShell };
11
+
12
+ export function shouldOpenOrchestratorShell({
13
+ interactive = Boolean(input.isTTY && output.isTTY),
14
+ json = false,
15
+ hasImplicitFlags = false
16
+ } = {}) {
17
+ if (!interactive || json) return false;
18
+ if (hasImplicitFlags) return false;
19
+ return canUseOrchestratorShell({ interactive });
20
+ }
21
+
22
+ export async function runOrchestratorShell({
23
+ packageRoot,
24
+ packageManifest,
25
+ workspaceRoot,
26
+ interactive = Boolean(input.isTTY && output.isTTY),
27
+ runOrchestratorInkImpl = defaultRunOrchestratorInk
28
+ }) {
29
+ if (!interactive) {
30
+ throw new Error(
31
+ `Non-interactive shell requires an explicit command. Try ${formatCliCommand("help")} or ${formatCliCommand("setup --yes")}.`
32
+ );
33
+ }
34
+
35
+ if (!canUseOrchestratorShell({ interactive })) {
36
+ throw new Error(
37
+ `Interactive shell requires a capable TTY. Use ${formatCliCommand("setup --simple")} or explicit commands.`
38
+ );
39
+ }
40
+
41
+ const homeDir = resolveHomeDir();
42
+ const outcome = await runOrchestratorInkImpl({
43
+ homeDir,
44
+ workspaceRoot,
45
+ packageRoot,
46
+ packageName: packageManifest.name,
47
+ cliVersion: packageManifest.version
48
+ });
49
+
50
+ if (outcome.error) {
51
+ throw outcome.error;
52
+ }
53
+
54
+ if (outcome.cancelled) {
55
+ return { cancelled: true, wrote: false };
56
+ }
57
+
58
+ if (outcome.confirmed && outcome.action === PLAN_ACTIONS.SETUP) {
59
+ const setupOutcome = await runGlobalSetup(
60
+ {
61
+ cwd: workspaceRoot,
62
+ adapters: null,
63
+ components: null,
64
+ noDefaultComponents: false,
65
+ dryRun: false,
66
+ yes: false,
67
+ confirm: false,
68
+ preflight: true,
69
+ preflightExplicit: false,
70
+ yesExplicit: false,
71
+ confirmExplicit: false,
72
+ json: false,
73
+ interactive: true,
74
+ simple: false
75
+ },
76
+ packageManifest,
77
+ packageRoot
78
+ );
79
+
80
+ return {
81
+ cancelled: Boolean(setupOutcome.cancelled),
82
+ wrote: !setupOutcome.cancelled && !setupOutcome.result?.dryRun,
83
+ action: PLAN_ACTIONS.SETUP,
84
+ setupOutcome
85
+ };
86
+ }
87
+
88
+ return { cancelled: false, wrote: false, action: outcome.action ?? null };
89
+ }
90
+
91
+ export async function runOrchestratorDiagnostics({
92
+ homeDir,
93
+ workspaceRoot,
94
+ packageName,
95
+ packageRoot,
96
+ cliVersion,
97
+ json = false
98
+ }) {
99
+ const diagnostics = await buildReadOnlyDiagnostics({
100
+ homeDir,
101
+ workspaceRoot,
102
+ packageName,
103
+ packageRoot,
104
+ cliVersion
105
+ });
106
+
107
+ if (json) {
108
+ return diagnostics;
109
+ }
110
+
111
+ console.log(commandHeader("orchestrator — agent capability diagnostics"));
112
+ console.log(`Home: ${homeDir}`);
113
+ console.log(`Workspace: ${workspaceRoot}`);
114
+ console.log("");
115
+
116
+ for (const capability of diagnostics.capabilities) {
117
+ console.log(
118
+ ` ${capability.label.padEnd(14)} ${capability.state.padEnd(14)} detected=${capability.detected ? "yes" : "no"}`
119
+ );
120
+ }
121
+
122
+ console.log("");
123
+ console.log("Recommendations:");
124
+ for (const recommendation of diagnostics.recommendations) {
125
+ console.log(` - ${recommendation}`);
126
+ }
127
+
128
+ return diagnostics;
129
+ }
130
+
131
+ function commandHeader(title) {
132
+ return `${BRAND.displayName} ${title}`;
133
+ }
134
+
135
+ export function assertPlanExecution(plan, { confirmed = false } = {}) {
136
+ if (!shouldExecutePlan(plan, { confirmed })) {
137
+ throw new Error("Plan declined. No writes or installations were performed.");
138
+ }
139
+ }
@@ -15,6 +15,7 @@ export function harnessHomePaths(homeDir) {
15
15
  root,
16
16
  statePath: join(root, "state.json"),
17
17
  policyPath: join(root, "policy.json"),
18
+ profilePath: join(root, "profile.json"),
18
19
  historyPath: join(root, "history.jsonl"),
19
20
  coreDir: join(root, "core"),
20
21
  backupsDir: join(root, "backups")
@@ -0,0 +1,146 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { harnessHomePaths } from "./paths.js";
5
+ import { AGENT_CAPABILITY_IDS } from "./agent-capabilities/index.js";
6
+
7
+ export const PROFILE_KEYS = new Set([
8
+ "coordinator",
9
+ "defaultAgents",
10
+ "defaultComponents",
11
+ "applyMode"
12
+ ]);
13
+
14
+ export const DEFAULT_PROFILE = {
15
+ coordinator: null,
16
+ defaultAgents: "detected",
17
+ defaultComponents: null,
18
+ applyMode: "prompt"
19
+ };
20
+
21
+ const APPLY_MODES = new Set(["prompt", "confirm"]);
22
+
23
+ export function getGlobalProfilePath(homeDir) {
24
+ return join(harnessHomePaths(homeDir).root, "profile.json");
25
+ }
26
+
27
+ export function getProjectProfilePath(workspaceRoot) {
28
+ return join(workspaceRoot, ".harness", "kairo.json");
29
+ }
30
+
31
+ export async function loadProfileFile(filePath) {
32
+ if (!existsSync(filePath)) return null;
33
+
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(await readFile(filePath, "utf8"));
37
+ } catch (error) {
38
+ throw new Error(`Invalid profile file at ${filePath}: ${error.message}`);
39
+ }
40
+
41
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
42
+ throw new Error(`Invalid profile file at ${filePath}: expected a JSON object.`);
43
+ }
44
+
45
+ return parsed;
46
+ }
47
+
48
+ export async function loadGlobalProfile(homeDir) {
49
+ return loadProfileFile(getGlobalProfilePath(homeDir));
50
+ }
51
+
52
+ export async function loadProjectProfile(workspaceRoot) {
53
+ return loadProfileFile(getProjectProfilePath(workspaceRoot));
54
+ }
55
+
56
+ export async function resolveProfile({ homeDir, workspaceRoot }) {
57
+ const globalRaw = await loadGlobalProfile(homeDir);
58
+ const projectRaw = await loadProjectProfile(workspaceRoot);
59
+
60
+ const merged = {
61
+ ...DEFAULT_PROFILE,
62
+ ...(globalRaw ?? {}),
63
+ ...(projectRaw ?? {})
64
+ };
65
+
66
+ validateProfile(merged);
67
+
68
+ return {
69
+ profile: merged,
70
+ sources: {
71
+ global: globalRaw ? getGlobalProfilePath(homeDir) : null,
72
+ project: projectRaw ? getProjectProfilePath(workspaceRoot) : null
73
+ }
74
+ };
75
+ }
76
+
77
+ export function buildProfileJson(resolved) {
78
+ const { profile, sources } = resolved;
79
+
80
+ return {
81
+ coordinator: profile.coordinator,
82
+ defaultAgents: profile.defaultAgents,
83
+ defaultComponents: profile.defaultComponents,
84
+ applyMode: profile.applyMode,
85
+ sources: {
86
+ global: sources.global,
87
+ project: sources.project,
88
+ precedence: "project overrides global overrides defaults"
89
+ }
90
+ };
91
+ }
92
+
93
+ export async function saveGlobalProfile(homeDir, profile) {
94
+ validateProfile(profile);
95
+ const { root } = harnessHomePaths(homeDir);
96
+ const profilePath = getGlobalProfilePath(homeDir);
97
+ await mkdir(root, { recursive: true });
98
+ await writeFile(profilePath, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
99
+ return profilePath;
100
+ }
101
+
102
+ export function resolveProfileAgents(profile, detectedAgentIds) {
103
+ if (profile.defaultAgents === "detected") {
104
+ return detectedAgentIds.length > 0 ? [...detectedAgentIds] : [...AGENT_CAPABILITY_IDS];
105
+ }
106
+
107
+ if (profile.defaultAgents === "all") {
108
+ return [...AGENT_CAPABILITY_IDS];
109
+ }
110
+
111
+ if (Array.isArray(profile.defaultAgents)) {
112
+ return profile.defaultAgents.filter((id) => AGENT_CAPABILITY_IDS.includes(id));
113
+ }
114
+
115
+ return [...AGENT_CAPABILITY_IDS];
116
+ }
117
+
118
+ function validateProfile(profile) {
119
+ if (profile.coordinator != null && !AGENT_CAPABILITY_IDS.includes(profile.coordinator)) {
120
+ throw new Error(`Unknown coordinator "${profile.coordinator}". Use ${AGENT_CAPABILITY_IDS.join(", ")} or null.`);
121
+ }
122
+
123
+ if (!APPLY_MODES.has(profile.applyMode)) {
124
+ throw new Error(`Invalid applyMode "${profile.applyMode}". Use prompt or confirm.`);
125
+ }
126
+
127
+ if (
128
+ profile.defaultAgents !== "detected"
129
+ && profile.defaultAgents !== "all"
130
+ && !Array.isArray(profile.defaultAgents)
131
+ ) {
132
+ throw new Error('Profile defaultAgents must be "detected", "all", or an agent list.');
133
+ }
134
+
135
+ if (Array.isArray(profile.defaultAgents)) {
136
+ for (const agent of profile.defaultAgents) {
137
+ if (!AGENT_CAPABILITY_IDS.includes(agent)) {
138
+ throw new Error(`Unknown agent "${agent}" in profile.`);
139
+ }
140
+ }
141
+ }
142
+
143
+ if (profile.defaultComponents != null && !Array.isArray(profile.defaultComponents)) {
144
+ throw new Error("Profile defaultComponents must be an array or null.");
145
+ }
146
+ }