@kal-elsam/kairo-runtime 0.2.1 → 0.2.3

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 (37) hide show
  1. package/README.md +13 -2
  2. package/package.json +1 -1
  3. package/scripts/runtime-mvp-smoke.sh +152 -0
  4. package/src/cli.js +97 -9
  5. package/src/global/brand/index.js +10 -0
  6. package/src/global/dashboard-guidance.js +66 -0
  7. package/src/global/initial-experience.js +34 -0
  8. package/src/global/ink/orchestrator-app.js +372 -82
  9. package/src/global/ink/orchestrator-state.js +196 -65
  10. package/src/global/ink/run-orchestrator-ink.js +2 -0
  11. package/src/global/ink/run-setup-ink.js +2 -0
  12. package/src/global/ink/setup-app.js +8 -4
  13. package/src/global/ink/setup-state.js +24 -2
  14. package/src/global/orchestrator.js +46 -42
  15. package/src/global/paths.js +13 -0
  16. package/src/global/profile.js +17 -2
  17. package/src/global/runtime/execution-adapters/claude.js +65 -0
  18. package/src/global/runtime/execution-adapters/codex.js +78 -0
  19. package/src/global/runtime/execution-adapters/create-execution-adapter.js +92 -0
  20. package/src/global/runtime/execution-adapters/cursor.js +104 -0
  21. package/src/global/runtime/execution-adapters/index.js +36 -0
  22. package/src/global/runtime/execution-adapters/opencode.js +38 -0
  23. package/src/global/runtime/run-cancel-signal.js +29 -0
  24. package/src/global/runtime/run-cli.js +221 -0
  25. package/src/global/runtime/run-events.js +144 -0
  26. package/src/global/runtime/run-handoff.js +71 -0
  27. package/src/global/runtime/run-liveness.js +28 -0
  28. package/src/global/runtime/run-manager.js +271 -0
  29. package/src/global/runtime/run-profile.js +93 -0
  30. package/src/global/runtime/run-redact.js +66 -0
  31. package/src/global/runtime/run-starting.js +13 -0
  32. package/src/global/runtime/run-store.js +159 -0
  33. package/src/global/runtime/run-supervisor-lock.js +37 -0
  34. package/src/global/runtime/run-supervisor-worker.js +12 -0
  35. package/src/global/runtime/run-supervisor.js +289 -0
  36. package/src/global/runtime/run-types.js +117 -0
  37. package/src/global/setup.js +13 -5
package/README.md CHANGED
@@ -24,13 +24,24 @@ commands) without depending on Pi as a runtime or adding a Pi adapter.
24
24
 
25
25
  ## Quick start
26
26
 
27
- Recommended entry — run Kairo Runtime in your terminal (interactive setup wizard in a TTY):
27
+ Recommended entry — run Kairo Runtime in your terminal:
28
28
 
29
29
  ```bash
30
30
  npx @kal-elsam/kairo-runtime
31
+ # or, after a global install:
32
+ kairo
31
33
  ```
32
34
 
33
- Preview without writing anything:
35
+ **First run** (no `~/.harness/state.json`): interactive onboarding → safe diagnosis →
36
+ setup with confirmation → operations dashboard.
37
+
38
+ **Later runs** (state present): operations dashboard with a stable purpose line and a
39
+ contextual next step (configure, enable intelligence, launch a run, or review problems).
40
+
41
+ Explicit commands and setup flags keep their current behavior (`kairo setup`,
42
+ `kairo --dry-run`, `kairo shell`, non-TTY scripts, etc.).
43
+
44
+ Preview setup without writing anything:
34
45
 
35
46
  ```bash
36
47
  npx @kal-elsam/kairo-runtime --dry-run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ KAIRO=(node "$ROOT/bin/kairo.js")
6
+ WORKSPACE="${SMOKE_WORKSPACE:-$ROOT}"
7
+ HOME_DIR="${HARNESS_HOME:-$(mktemp -d /tmp/kairo-runtime-smoke-XXXXXX)}"
8
+ export HARNESS_HOME="$HOME_DIR"
9
+ export HARNESS_INK=0
10
+
11
+ PROMPT_A="List the files in the current directory and summarize in one sentence."
12
+ PROMPT_B="Reply with exactly the single word OK and nothing else."
13
+ PROMPT_SECRET="SMOKE_SECRET_PROMPT_$(date +%s)_do_not_persist"
14
+ SMOKE_MODEL="${SMOKE_MODEL:-}"
15
+
16
+ MODEL_ARGS=()
17
+ if [[ -n "$SMOKE_MODEL" ]]; then
18
+ MODEL_ARGS=(--model "$SMOKE_MODEL")
19
+ fi
20
+
21
+ log() { printf '[smoke] %s\n' "$*"; }
22
+
23
+ assert_no_prompt() {
24
+ local dir=$1 prompt=$2
25
+ if grep -qF "$prompt" "$dir/state.json" 2>/dev/null; then
26
+ echo "FAIL: prompt found in state.json for $dir" >&2
27
+ return 1
28
+ fi
29
+ if grep -qF "$prompt" "$dir/events.jsonl" 2>/dev/null; then
30
+ echo "FAIL: prompt found in events.jsonl for $dir" >&2
31
+ return 1
32
+ fi
33
+ if [[ -f "$dir/handoff.json" ]]; then
34
+ echo "FAIL: handoff.json still present for $dir" >&2
35
+ return 1
36
+ fi
37
+ }
38
+
39
+ log "HARNESS_HOME=$HOME_DIR"
40
+ log "WORKSPACE=$WORKSPACE"
41
+ if [[ -n "$SMOKE_MODEL" ]]; then
42
+ log "SMOKE_MODEL=$SMOKE_MODEL"
43
+ else
44
+ log "SMOKE_MODEL=(not set — Case 2 may fail if Codex default model is incompatible)"
45
+ fi
46
+
47
+ # --- Case 1: detached run + cross-process list/show/stop ---
48
+ log "=== Case 1: --no-wait + cross-process supervision ==="
49
+ START_A=$(python3 -c 'import time; print(int(time.time()*1000))')
50
+ OUT_A=$("${KAIRO[@]}" run --agent codex --task "$PROMPT_A" --cwd "$WORKSPACE" "${MODEL_ARGS[@]}" --no-wait 2>&1)
51
+ END_A=$(python3 -c 'import time; print(int(time.time()*1000))')
52
+ DUR_A=$((END_A - START_A))
53
+
54
+ RUN_ID=$(printf '%s\n' "$OUT_A" | sed -n 's/.*\(run_[a-z0-9_]*\).*/\1/p' | head -1)
55
+ if [[ -z "$RUN_ID" ]]; then
56
+ echo "FAIL: could not parse runId from output:" >&2
57
+ echo "$OUT_A" >&2
58
+ exit 1
59
+ fi
60
+
61
+ log "Case 1 runId=$RUN_ID parent_return_ms=$DUR_A"
62
+ if (( DUR_A > 5000 )); then
63
+ echo "FAIL: --no-wait took ${DUR_A}ms (>5000ms)" >&2
64
+ exit 1
65
+ fi
66
+
67
+ sleep 1
68
+ LIST_B=$("${KAIRO[@]}" runs list --cwd "$WORKSPACE" 2>&1)
69
+ if printf '%s\n' "$LIST_B" | grep -q interrupted; then
70
+ echo "FAIL: runs list shows interrupted" >&2
71
+ echo "$LIST_B" >&2
72
+ exit 1
73
+ fi
74
+ if ! printf '%s\n' "$LIST_B" | grep -q "$RUN_ID"; then
75
+ echo "FAIL: run not listed" >&2
76
+ echo "$LIST_B" >&2
77
+ exit 1
78
+ fi
79
+ if ! printf '%s\n' "$LIST_B" | grep -Eq "running|starting"; then
80
+ echo "FAIL: run not active in list" >&2
81
+ echo "$LIST_B" >&2
82
+ exit 1
83
+ fi
84
+
85
+ SHOW_B=$("${KAIRO[@]}" runs show "$RUN_ID" --cwd "$WORKSPACE" 2>&1)
86
+ if printf '%s\n' "$SHOW_B" | grep -q interrupted; then
87
+ echo "FAIL: runs show reports interrupted" >&2
88
+ echo "$SHOW_B" >&2
89
+ exit 1
90
+ fi
91
+
92
+ START_STOP=$(python3 -c 'import time; print(int(time.time()*1000))')
93
+ STOP_B=$("${KAIRO[@]}" runs stop "$RUN_ID" --cwd "$WORKSPACE" 2>&1)
94
+ END_STOP=$(python3 -c 'import time; print(int(time.time()*1000))')
95
+ DUR_STOP=$((END_STOP - START_STOP))
96
+
97
+ sleep 1
98
+ FINAL_A=$("${KAIRO[@]}" runs show "$RUN_ID" --json --cwd "$WORKSPACE")
99
+ STATE_A=$(printf '%s\n' "$FINAL_A" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{const j=JSON.parse(s);console.log(j.metadata.state)})')
100
+ if [[ "$STATE_A" != "cancelled" ]]; then
101
+ echo "FAIL: expected cancelled, got $STATE_A" >&2
102
+ exit 1
103
+ fi
104
+
105
+ RUN_DIR_A="$HOME_DIR/.harness/runs/$RUN_ID"
106
+ assert_no_prompt "$RUN_DIR_A" "$PROMPT_A"
107
+ log "Case 1 PASS state=$STATE_A stop_ms=$DUR_STOP"
108
+
109
+ # --- Case 2: normal completion (must be completed) ---
110
+ log "=== Case 2: wait for normal completion ==="
111
+ START_B=$(python3 -c 'import time; print(int(time.time()*1000))')
112
+ OUT_B=$("${KAIRO[@]}" run --agent codex --task "$PROMPT_B" --cwd "$WORKSPACE" --permissions yolo "${MODEL_ARGS[@]}" 2>&1)
113
+ END_B=$(python3 -c 'import time; print(int(time.time()*1000))')
114
+ DUR_B=$((END_B - START_B))
115
+
116
+ RUN_ID_B=$(printf '%s\n' "$OUT_B" | sed -n 's/.*\(run_[a-z0-9_]*\).*/\1/p' | head -1)
117
+ FINAL_B=$("${KAIRO[@]}" runs show "$RUN_ID_B" --json --cwd "$WORKSPACE")
118
+ STATE_B=$(printf '%s\n' "$FINAL_B" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{const j=JSON.parse(s);console.log(j.metadata.state)})')
119
+
120
+ if [[ "$STATE_B" != "completed" ]]; then
121
+ echo "FAIL: expected completed, got $STATE_B" >&2
122
+ echo "$OUT_B" >&2
123
+ if [[ -z "$SMOKE_MODEL" ]]; then
124
+ echo "Hint: set SMOKE_MODEL to a Codex model compatible with your account/CLI version." >&2
125
+ fi
126
+ exit 1
127
+ fi
128
+
129
+ RUN_DIR_B="$HOME_DIR/.harness/runs/$RUN_ID_B"
130
+ assert_no_prompt "$RUN_DIR_B" "$PROMPT_B"
131
+ log "Case 2 PASS runId=$RUN_ID_B state=$STATE_B duration_ms=$DUR_B"
132
+
133
+ # --- Case 3: privacy spot-check with unique secret prompt ---
134
+ log "=== Case 3: privacy secret prompt ==="
135
+ START_C=$(python3 -c 'import time; print(int(time.time()*1000))')
136
+ OUT_C=$("${KAIRO[@]}" run --agent codex --task "$PROMPT_SECRET" --cwd "$WORKSPACE" --permissions yolo "${MODEL_ARGS[@]}" --no-wait 2>&1)
137
+ RUN_ID_C=$(printf '%s\n' "$OUT_C" | sed -n 's/.*\(run_[a-z0-9_]*\).*/\1/p' | head -1)
138
+ sleep 2
139
+ "${KAIRO[@]}" runs stop "$RUN_ID_C" --cwd "$WORKSPACE" >/dev/null 2>&1 || true
140
+ sleep 1
141
+ RUN_DIR_C="$HOME_DIR/.harness/runs/$RUN_ID_C"
142
+ assert_no_prompt "$RUN_DIR_C" "$PROMPT_SECRET"
143
+ END_C=$(python3 -c 'import time; print(int(time.time()*1000))')
144
+ DUR_C=$((END_C - START_C))
145
+ log "Case 3 PASS runId=$RUN_ID_C duration_ms=$DUR_C"
146
+
147
+ printf '\n=== SMOKE SUMMARY ===\n'
148
+ printf 'Case 1 (detach+stop): runId=%s state=%s parent_ms=%s stop_ms=%s\n' "$RUN_ID" "$STATE_A" "$DUR_A" "$DUR_STOP"
149
+ printf 'Case 2 (complete): runId=%s state=%s duration_ms=%s model=%s\n' "$RUN_ID_B" "$STATE_B" "$DUR_B" "${SMOKE_MODEL:-default}"
150
+ printf 'Case 3 (privacy): runId=%s duration_ms=%s\n' "$RUN_ID_C" "$DUR_C"
151
+ printf 'HARNESS_HOME=%s\n' "$HOME_DIR"
152
+ printf 'ALL PASS\n'
package/src/cli.js CHANGED
@@ -32,6 +32,7 @@ import { resolveHomeDir } from "./global/paths.js";
32
32
  import { runWorkspaceDetect, runWorkspaceDoctor, runWorkspaceInit, runWorkspaceUpdate } from "./workspace-cli.js";
33
33
  import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orchestrator.js";
34
34
  import { runIntelligenceCli } from "./global/intelligence-cli.js";
35
+ import { runGlobalRun, runGlobalRuns } from "./global/runtime/run-cli.js";
35
36
  import {
36
37
  LEGACY_PACKAGE_NAME,
37
38
  PACKAGE_NAME,
@@ -41,6 +42,11 @@ import {
41
42
  resolveSuggestedInvocation
42
43
  } from "./global/brand/cli.js";
43
44
  import { BRAND } from "./global/brand/index.js";
45
+ import {
46
+ INITIAL_EXPERIENCE,
47
+ hasConfiguredGlobalState,
48
+ resolveInitialExperience
49
+ } from "./global/initial-experience.js";
44
50
 
45
51
  export { resolveSuggestedInvocation };
46
52
 
@@ -49,7 +55,7 @@ const packageRoot = resolve(__dirname, "..");
49
55
  const SCOPES = new Set(["agent-global", "workspace"]);
50
56
 
51
57
  export async function runCli(argv) {
52
- const { command, options } = parseArgs(argv);
58
+ const { command, options, isImplicitCommand } = parseArgs(argv);
53
59
  maybeWarnLegacyCli(process.argv, { json: options.json });
54
60
 
55
61
  if (options.help || command === "help") {
@@ -68,14 +74,22 @@ export async function runCli(argv) {
68
74
  const invoke = resolveSuggestedInvocation(packageManifest.name);
69
75
 
70
76
  switch (command) {
71
- case "shell":
77
+ case "shell": {
78
+ const homeDir = resolveHomeDir();
79
+ const resolvedMode = resolveInitialExperience({
80
+ interactive: optionsWithPolicy.interactive,
81
+ isImplicitCommand,
82
+ hasGlobalState: hasConfiguredGlobalState(homeDir)
83
+ });
72
84
  await runOrchestratorShell({
73
85
  packageRoot,
74
86
  packageManifest,
75
87
  workspaceRoot: optionsWithPolicy.cwd,
76
- interactive: optionsWithPolicy.interactive
88
+ interactive: optionsWithPolicy.interactive,
89
+ initialMode: resolvedMode ?? INITIAL_EXPERIENCE.DASHBOARD
77
90
  });
78
91
  return;
92
+ }
79
93
  case "orchestrator":
80
94
  await runOrchestratorDiagnostics({
81
95
  homeDir: resolveHomeDir(),
@@ -86,6 +100,12 @@ export async function runCli(argv) {
86
100
  json: optionsWithPolicy.json
87
101
  });
88
102
  return;
103
+ case "run":
104
+ await runGlobalRun(optionsWithPolicy, packageManifest);
105
+ return;
106
+ case "runs":
107
+ await runGlobalRuns(optionsWithPolicy, packageManifest);
108
+ return;
89
109
  case "intelligence":
90
110
  await runIntelligenceCli(optionsWithPolicy, packageManifest);
91
111
  return;
@@ -333,6 +353,17 @@ export function parseArgs(argv) {
333
353
  intelligenceTask: null,
334
354
  intelligencePrompt: null,
335
355
  intelligencePaths: [],
356
+ runsAction: null,
357
+ runId: null,
358
+ agent: null,
359
+ task: null,
360
+ model: null,
361
+ permissions: null,
362
+ captureTranscript: false,
363
+ follow: false,
364
+ wait: true,
365
+ activeOnly: false,
366
+ timeoutMs: null,
336
367
  includePrivate: false,
337
368
  cloudConsent: false
338
369
  };
@@ -349,6 +380,10 @@ export function parseArgs(argv) {
349
380
  parseHistoryAction(args, options);
350
381
  }
351
382
 
383
+ if (command === "runs") {
384
+ parseRunsAction(args, options);
385
+ }
386
+
352
387
  if (command === "intelligence") {
353
388
  parseIntelligenceAction(args, options);
354
389
  }
@@ -414,12 +449,27 @@ export function parseArgs(argv) {
414
449
  else if (arg === "--out") options.outPath = resolve(args[++index]);
415
450
  else if (arg.startsWith("--out=")) options.outPath = resolve(arg.slice("--out=".length));
416
451
  else if (arg === "--simple") options.simple = true;
417
- else if (arg === "--task") options.intelligenceTask = args[++index];
418
- else if (arg.startsWith("--task=")) options.intelligenceTask = arg.slice("--task=".length);
452
+ else if (arg === "--task" || arg.startsWith("--task=")) {
453
+ const taskValue = arg.startsWith("--task=") ? arg.slice("--task=".length) : args[++index];
454
+ if (command === "run") options.task = taskValue;
455
+ else options.intelligenceTask = taskValue;
456
+ }
419
457
  else if (arg === "--prompt") options.intelligencePrompt = args[++index];
420
458
  else if (arg.startsWith("--prompt=")) options.intelligencePrompt = arg.slice("--prompt=".length);
421
459
  else if (arg === "--paths") options.intelligencePaths = parsePathList(args[++index]);
422
460
  else if (arg.startsWith("--paths=")) options.intelligencePaths = parsePathList(arg.slice("--paths=".length));
461
+ else if (arg === "--agent") options.agent = args[++index];
462
+ else if (arg.startsWith("--agent=")) options.agent = arg.slice("--agent=".length);
463
+ else if (arg === "--model") options.model = args[++index];
464
+ else if (arg.startsWith("--model=")) options.model = arg.slice("--model=".length);
465
+ else if (arg === "--permissions") options.permissions = parsePathList(args[++index]);
466
+ else if (arg.startsWith("--permissions=")) options.permissions = parsePathList(arg.slice("--permissions=".length));
467
+ else if (arg === "--capture-transcript") options.captureTranscript = true;
468
+ else if (arg === "--follow") options.follow = true;
469
+ else if (arg === "--no-wait") options.wait = false;
470
+ else if (arg === "--active-only") options.activeOnly = true;
471
+ else if (arg === "--timeout") options.timeoutMs = parsePositiveInt(args[++index], "timeout") * 1000;
472
+ else if (arg.startsWith("--timeout=")) options.timeoutMs = parsePositiveInt(arg.slice("--timeout=".length), "timeout") * 1000;
423
473
  else if (arg === "--include-private") options.includePrivate = true;
424
474
  else if (arg === "--cloud-consent") options.cloudConsent = true;
425
475
  else if (arg === "--help" || arg === "-h") options.help = true;
@@ -427,7 +477,11 @@ export function parseArgs(argv) {
427
477
  else throw new Error(`Unknown option "${arg}".`);
428
478
  }
429
479
 
430
- return { command, options };
480
+ if (command === "run" && !options.task && args.length > 0) {
481
+ options.task = args.join(" ").trim();
482
+ }
483
+
484
+ return { command, options, isImplicitCommand: implicitCommand };
431
485
  }
432
486
 
433
487
  function parseComponentsAction(args, options) {
@@ -512,6 +566,30 @@ function parseHistoryAction(args, options) {
512
566
  throw new Error(`Unknown history action "${action}". Use last or omit for the full log.`);
513
567
  }
514
568
 
569
+ function parseRunsAction(args, options) {
570
+ const action = args[0];
571
+
572
+ if (!action || action.startsWith("-")) {
573
+ options.runsAction = "list";
574
+ return;
575
+ }
576
+
577
+ if (!new Set(["list", "show", "stop"]).has(action)) {
578
+ throw new Error(`Unknown runs action "${action}". Use list, show, or stop.`);
579
+ }
580
+
581
+ args.shift();
582
+ options.runsAction = action;
583
+
584
+ if (action === "show" || action === "stop") {
585
+ const runId = args[0];
586
+ if (!runId || runId.startsWith("-")) {
587
+ throw new Error(`Missing run id. Use: ${formatCliCommand(`runs ${action} <runId>`)}`);
588
+ }
589
+ options.runId = args.shift();
590
+ }
591
+ }
592
+
515
593
  function parseIntelligenceAction(args, options) {
516
594
  const action = args[0];
517
595
  if (!action || action.startsWith("-")) {
@@ -559,6 +637,8 @@ function normalizeCommand(command) {
559
637
  if (command === "install" || command === "i") return "install";
560
638
  if (command === "shell") return "shell";
561
639
  if (command === "orchestrator") return "orchestrator";
640
+ if (command === "run") return "run";
641
+ if (command === "runs") return "runs";
562
642
  if (command === "intelligence" || command === "intel") return "intelligence";
563
643
  if (command === "setup") return "setup";
564
644
  if (command === "status") return "status";
@@ -616,10 +696,15 @@ sections, components, backups, and drift repair under ~/.harness.
616
696
  Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
617
697
 
618
698
  Usage:
619
- ${cli} Interactive orchestrator shell (TTY)
699
+ ${cli} First run: onboarding → setup → dashboard (TTY).
700
+ Later: operations dashboard with next-step guidance.
620
701
  ${cli} --dry-run Setup dry-run (scriptable)
621
702
  ${cli} --version
622
- ${cli} shell Interactive orchestrator shell (TTY)
703
+ ${cli} shell Operations dashboard (TTY)
704
+ ${cli} run --agent <id> --task "..." [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
705
+ ${cli} runs list [--json] [--limit <n>] [--active-only]
706
+ ${cli} runs show <runId> [--json] [--limit <n>] [--follow]
707
+ ${cli} runs stop <runId> [--json]
623
708
  ${cli} orchestrator [--json] Read-only agent capability diagnostics
624
709
  ${cli} intelligence [status|models|context|route|ask] [--json]
625
710
  ${cli} intelligence ask --prompt "..." [--cloud-consent] [--yes] [--paths a,b]
@@ -656,7 +741,10 @@ Scopes:
656
741
  Explicit --scope=workspace only.
657
742
 
658
743
  Commands:
659
- shell Interactive Ink orchestrator (TTY). Bare ${cli} opens this in TTY sessions.
744
+ shell Operations dashboard (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
745
+ is missing, otherwise the dashboard. Explicit ${cli} shell always opens the dashboard.
746
+ run Launch a managed agent run with local audit trail.
747
+ runs List, inspect, or cancel agent runs under ~/.harness/runs/.
660
748
  orchestrator Read-only capability registry diagnostics (--json supported).
661
749
  intelligence Harness Engineering layer: backends, context packs, routing, budgets.
662
750
  Local-first (Ollama). Cloud (OpenRouter/free) only with --cloud-consent.
@@ -70,6 +70,16 @@ export const WIZARD_COPY = {
70
70
  coreOnlyLabel: "Core only (no components)"
71
71
  };
72
72
 
73
+ /** First-run framing reused by onboarding → setup. */
74
+ export const ONBOARDING_COPY = {
75
+ welcomeTitle: `Welcome to ${BRAND.displayName}`,
76
+ purpose:
77
+ `${BRAND.displayName} detects, configures, and coordinates the local agents you already use.`,
78
+ safety:
79
+ "Diagnosis is read-only. Nothing is modified until you confirm a plan.",
80
+ continueHint: "Press Enter to diagnose and configure · Esc to exit"
81
+ };
82
+
73
83
  export function getAgentLabel(agentId) {
74
84
  return AGENT_LABELS[agentId] ?? agentId;
75
85
  }
@@ -0,0 +1,66 @@
1
+ import { formatCliCommand } from "./brand/cli.js";
2
+
3
+ export const DASHBOARD_PURPOSE =
4
+ "Detects, configures, and coordinates local AI agents — no changes without confirmation.";
5
+
6
+ export const NEXT_STEP_KINDS = {
7
+ CONFIGURE: "configure",
8
+ ENABLE_INTELLIGENCE: "enable_intelligence",
9
+ LAUNCH: "launch",
10
+ REVIEW: "review"
11
+ };
12
+
13
+ export function formatDashboardPurpose() {
14
+ return DASHBOARD_PURPOSE;
15
+ }
16
+
17
+ /**
18
+ * Contextual next step from existing diagnostics + dashboard snapshot.
19
+ * Priority: configure → review problems → enable intelligence → launch.
20
+ */
21
+ export function resolveDashboardRecommendation({
22
+ hasGlobalState = false,
23
+ diagnostics = null,
24
+ dashboard = null
25
+ } = {}) {
26
+ const summary = diagnostics?.diagnostics ?? { detected: 0, errors: 0 };
27
+ const intelligence = diagnostics?.intelligence?.summary;
28
+ const launchableCount = (dashboard?.providers ?? []).filter((entry) => entry.launchable).length;
29
+ const hasErrors = (summary.errors ?? 0) > 0;
30
+ const hasProblemRecommendation = (diagnostics?.recommendations ?? []).some((line) =>
31
+ /error|fix|drift|not detected|failed|problem/i.test(line)
32
+ );
33
+
34
+ if (!hasGlobalState || (summary.detected ?? 0) === 0) {
35
+ return {
36
+ kind: NEXT_STEP_KINDS.CONFIGURE,
37
+ message: `Configure the local environment with ${formatCliCommand("setup")}.`
38
+ };
39
+ }
40
+
41
+ if (hasErrors || hasProblemRecommendation) {
42
+ return {
43
+ kind: NEXT_STEP_KINDS.REVIEW,
44
+ message: "Review diagnostics for problems before launching a run."
45
+ };
46
+ }
47
+
48
+ if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
49
+ return {
50
+ kind: NEXT_STEP_KINDS.ENABLE_INTELLIGENCE,
51
+ message: "Enable intelligence: start Ollama or set OPENROUTER_API_KEY, then retry."
52
+ };
53
+ }
54
+
55
+ if (launchableCount > 0) {
56
+ return {
57
+ kind: NEXT_STEP_KINDS.LAUNCH,
58
+ message: "Launch a supervised run from the menu or with kairo run."
59
+ };
60
+ }
61
+
62
+ return {
63
+ kind: NEXT_STEP_KINDS.REVIEW,
64
+ message: "Review diagnostics for problems before launching a run."
65
+ };
66
+ }
@@ -0,0 +1,34 @@
1
+ import { existsSync } from "node:fs";
2
+ import { harnessHomePaths } from "./paths.js";
3
+
4
+ export const INITIAL_EXPERIENCE = {
5
+ ONBOARDING: "onboarding",
6
+ DASHBOARD: "dashboard"
7
+ };
8
+
9
+ /**
10
+ * First-run marker is ~/.harness/state.json only.
11
+ * profile.json does not participate in this decision.
12
+ */
13
+ export function hasConfiguredGlobalState(homeDir) {
14
+ return existsSync(harnessHomePaths(homeDir).statePath);
15
+ }
16
+
17
+ /**
18
+ * Pure resolver for the interactive bare-entry experience.
19
+ * Returns null when CLI should keep existing non-onboarding paths
20
+ * (non-TTY, explicit commands, setup flags already routed elsewhere).
21
+ */
22
+ export function resolveInitialExperience({
23
+ interactive = false,
24
+ isImplicitCommand = false,
25
+ hasGlobalState = false
26
+ } = {}) {
27
+ if (!interactive || !isImplicitCommand) {
28
+ return null;
29
+ }
30
+
31
+ return hasGlobalState
32
+ ? INITIAL_EXPERIENCE.DASHBOARD
33
+ : INITIAL_EXPERIENCE.ONBOARDING;
34
+ }