@kal-elsam/kairo-runtime 0.2.1 → 0.2.2

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.2.1",
3
+ "version": "0.2.2",
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,
@@ -86,6 +87,12 @@ export async function runCli(argv) {
86
87
  json: optionsWithPolicy.json
87
88
  });
88
89
  return;
90
+ case "run":
91
+ await runGlobalRun(optionsWithPolicy, packageManifest);
92
+ return;
93
+ case "runs":
94
+ await runGlobalRuns(optionsWithPolicy, packageManifest);
95
+ return;
89
96
  case "intelligence":
90
97
  await runIntelligenceCli(optionsWithPolicy, packageManifest);
91
98
  return;
@@ -333,6 +340,17 @@ export function parseArgs(argv) {
333
340
  intelligenceTask: null,
334
341
  intelligencePrompt: null,
335
342
  intelligencePaths: [],
343
+ runsAction: null,
344
+ runId: null,
345
+ agent: null,
346
+ task: null,
347
+ model: null,
348
+ permissions: null,
349
+ captureTranscript: false,
350
+ follow: false,
351
+ wait: true,
352
+ activeOnly: false,
353
+ timeoutMs: null,
336
354
  includePrivate: false,
337
355
  cloudConsent: false
338
356
  };
@@ -349,6 +367,10 @@ export function parseArgs(argv) {
349
367
  parseHistoryAction(args, options);
350
368
  }
351
369
 
370
+ if (command === "runs") {
371
+ parseRunsAction(args, options);
372
+ }
373
+
352
374
  if (command === "intelligence") {
353
375
  parseIntelligenceAction(args, options);
354
376
  }
@@ -414,12 +436,27 @@ export function parseArgs(argv) {
414
436
  else if (arg === "--out") options.outPath = resolve(args[++index]);
415
437
  else if (arg.startsWith("--out=")) options.outPath = resolve(arg.slice("--out=".length));
416
438
  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);
439
+ else if (arg === "--task" || arg.startsWith("--task=")) {
440
+ const taskValue = arg.startsWith("--task=") ? arg.slice("--task=".length) : args[++index];
441
+ if (command === "run") options.task = taskValue;
442
+ else options.intelligenceTask = taskValue;
443
+ }
419
444
  else if (arg === "--prompt") options.intelligencePrompt = args[++index];
420
445
  else if (arg.startsWith("--prompt=")) options.intelligencePrompt = arg.slice("--prompt=".length);
421
446
  else if (arg === "--paths") options.intelligencePaths = parsePathList(args[++index]);
422
447
  else if (arg.startsWith("--paths=")) options.intelligencePaths = parsePathList(arg.slice("--paths=".length));
448
+ else if (arg === "--agent") options.agent = args[++index];
449
+ else if (arg.startsWith("--agent=")) options.agent = arg.slice("--agent=".length);
450
+ else if (arg === "--model") options.model = args[++index];
451
+ else if (arg.startsWith("--model=")) options.model = arg.slice("--model=".length);
452
+ else if (arg === "--permissions") options.permissions = parsePathList(args[++index]);
453
+ else if (arg.startsWith("--permissions=")) options.permissions = parsePathList(arg.slice("--permissions=".length));
454
+ else if (arg === "--capture-transcript") options.captureTranscript = true;
455
+ else if (arg === "--follow") options.follow = true;
456
+ else if (arg === "--no-wait") options.wait = false;
457
+ else if (arg === "--active-only") options.activeOnly = true;
458
+ else if (arg === "--timeout") options.timeoutMs = parsePositiveInt(args[++index], "timeout") * 1000;
459
+ else if (arg.startsWith("--timeout=")) options.timeoutMs = parsePositiveInt(arg.slice("--timeout=".length), "timeout") * 1000;
423
460
  else if (arg === "--include-private") options.includePrivate = true;
424
461
  else if (arg === "--cloud-consent") options.cloudConsent = true;
425
462
  else if (arg === "--help" || arg === "-h") options.help = true;
@@ -427,6 +464,10 @@ export function parseArgs(argv) {
427
464
  else throw new Error(`Unknown option "${arg}".`);
428
465
  }
429
466
 
467
+ if (command === "run" && !options.task && args.length > 0) {
468
+ options.task = args.join(" ").trim();
469
+ }
470
+
430
471
  return { command, options };
431
472
  }
432
473
 
@@ -512,6 +553,30 @@ function parseHistoryAction(args, options) {
512
553
  throw new Error(`Unknown history action "${action}". Use last or omit for the full log.`);
513
554
  }
514
555
 
556
+ function parseRunsAction(args, options) {
557
+ const action = args[0];
558
+
559
+ if (!action || action.startsWith("-")) {
560
+ options.runsAction = "list";
561
+ return;
562
+ }
563
+
564
+ if (!new Set(["list", "show", "stop"]).has(action)) {
565
+ throw new Error(`Unknown runs action "${action}". Use list, show, or stop.`);
566
+ }
567
+
568
+ args.shift();
569
+ options.runsAction = action;
570
+
571
+ if (action === "show" || action === "stop") {
572
+ const runId = args[0];
573
+ if (!runId || runId.startsWith("-")) {
574
+ throw new Error(`Missing run id. Use: ${formatCliCommand(`runs ${action} <runId>`)}`);
575
+ }
576
+ options.runId = args.shift();
577
+ }
578
+ }
579
+
515
580
  function parseIntelligenceAction(args, options) {
516
581
  const action = args[0];
517
582
  if (!action || action.startsWith("-")) {
@@ -559,6 +624,8 @@ function normalizeCommand(command) {
559
624
  if (command === "install" || command === "i") return "install";
560
625
  if (command === "shell") return "shell";
561
626
  if (command === "orchestrator") return "orchestrator";
627
+ if (command === "run") return "run";
628
+ if (command === "runs") return "runs";
562
629
  if (command === "intelligence" || command === "intel") return "intelligence";
563
630
  if (command === "setup") return "setup";
564
631
  if (command === "status") return "status";
@@ -619,7 +686,11 @@ Usage:
619
686
  ${cli} Interactive orchestrator shell (TTY)
620
687
  ${cli} --dry-run Setup dry-run (scriptable)
621
688
  ${cli} --version
622
- ${cli} shell Interactive orchestrator shell (TTY)
689
+ ${cli} shell Operations dashboard (TTY)
690
+ ${cli} run --agent <id> --task "..." [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
691
+ ${cli} runs list [--json] [--limit <n>] [--active-only]
692
+ ${cli} runs show <runId> [--json] [--limit <n>] [--follow]
693
+ ${cli} runs stop <runId> [--json]
623
694
  ${cli} orchestrator [--json] Read-only agent capability diagnostics
624
695
  ${cli} intelligence [status|models|context|route|ask] [--json]
625
696
  ${cli} intelligence ask --prompt "..." [--cloud-consent] [--yes] [--paths a,b]
@@ -656,7 +727,9 @@ Scopes:
656
727
  Explicit --scope=workspace only.
657
728
 
658
729
  Commands:
659
- shell Interactive Ink orchestrator (TTY). Bare ${cli} opens this in TTY sessions.
730
+ shell Operations dashboard (TTY). Bare ${cli} opens this in TTY sessions.
731
+ run Launch a managed agent run with local audit trail.
732
+ runs List, inspect, or cancel agent runs under ~/.harness/runs/.
660
733
  orchestrator Read-only capability registry diagnostics (--json supported).
661
734
  intelligence Harness Engineering layer: backends, context packs, routing, budgets.
662
735
  Local-first (Ollama). Cloud (OpenRouter/free) only with --cloud-consent.