@kal-elsam/kairo-runtime 0.5.1 → 0.7.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 (44) hide show
  1. package/README.md +44 -6
  2. package/global-template/components/catalog.json +4 -2
  3. package/global-template/components/orchestrator/orchestrator.md +1 -1
  4. package/global-template/core/orchestrator.md +1 -1
  5. package/package.json +3 -2
  6. package/scripts/cockpit-smoke.mjs +1 -1
  7. package/scripts/install.sh +1 -1
  8. package/scripts/installer-smoke-test.sh +6 -4
  9. package/src/cli.js +60 -1
  10. package/src/global/adapters/pi.js +53 -0
  11. package/src/global/agent-capabilities/index.js +11 -1
  12. package/src/global/brand/index.js +4 -2
  13. package/src/global/dashboard-guidance.js +1 -1
  14. package/src/global/global-cli.js +1 -1
  15. package/src/global/ink/cockpit-controller.js +10 -0
  16. package/src/global/ink/cockpit-focus.js +18 -3
  17. package/src/global/ink/cockpit-models.js +8 -1
  18. package/src/global/ink/cockpit-reviews.js +62 -0
  19. package/src/global/ink/cockpit-runs.js +11 -2
  20. package/src/global/ink/cockpit-views.js +19 -2
  21. package/src/global/ink/orchestrator-app.js +23 -3
  22. package/src/global/ink/orchestrator-state.js +2 -0
  23. package/src/global/ink/use-orchestrator-data.js +30 -0
  24. package/src/global/integrations/engram-evidence.js +41 -3
  25. package/src/global/integrations/sdd-destinations.js +2 -2
  26. package/src/global/paths.js +1 -0
  27. package/src/global/registry.js +2 -1
  28. package/src/global/runtime/execution-adapters/codex.js +2 -1
  29. package/src/global/runtime/execution-adapters/create-execution-adapter.js +1 -0
  30. package/src/global/runtime/execution-adapters/index.js +3 -1
  31. package/src/global/runtime/execution-adapters/pi.js +184 -0
  32. package/src/global/runtime/review/index.js +37 -0
  33. package/src/global/runtime/review/review-cli.js +150 -0
  34. package/src/global/runtime/review/review-codex.js +132 -0
  35. package/src/global/runtime/review/review-exec.js +136 -0
  36. package/src/global/runtime/review/review-fs.js +52 -0
  37. package/src/global/runtime/review/review-git.js +212 -0
  38. package/src/global/runtime/review/review-patch.js +122 -0
  39. package/src/global/runtime/review/review-pi.js +168 -0
  40. package/src/global/runtime/review/review-receipts.js +128 -0
  41. package/src/global/runtime/review/review-runner.js +119 -0
  42. package/src/global/runtime/review/review-types.js +108 -0
  43. package/src/global/runtime/review/review-validate.js +280 -0
  44. package/src/global/runtime/write-atomic-json.js +24 -20
package/README.md CHANGED
@@ -261,7 +261,7 @@ node ./bin/kairo.js install --dry-run
261
261
 
262
262
  ## Supported adapters (agent-global)
263
263
 
264
- Kairo Runtime does **not** install Cursor, Codex, OpenCode, or Claude Code. It detects
264
+ Kairo Runtime does **not** install Cursor, Codex, OpenCode, Claude Code, or Pi. It detects
265
265
  their home-directory roots and writes managed sections into their config files.
266
266
 
267
267
  | Adapter | Label | Root | Config file |
@@ -270,6 +270,7 @@ their home-directory roots and writes managed sections into their config files.
270
270
  | `codex` | Codex | `~/.codex` | `~/.codex/AGENTS.md` |
271
271
  | `opencode` | OpenCode | `~/.config/opencode` | `~/.config/opencode/AGENTS.md` |
272
272
  | `claude` | Claude Code | `~/.claude` | `~/.claude/CLAUDE.md` |
273
+ | `pi` | Pi | `~/.pi/agent` | `~/.pi/agent/AGENTS.md` |
273
274
 
274
275
  Inspect detection and managed state:
275
276
 
@@ -281,14 +282,49 @@ kairo adapters --json
281
282
  Agent selection defaults:
282
283
 
283
284
  - If agent roots are detected → configure detected agents only.
284
- - If none are detected → safe fallback to all four supported adapters.
285
- - Force all four explicitly:
285
+ - If none are detected → safe fallback to all five supported adapters.
286
+ - Force all five explicitly:
286
287
 
287
288
  ```bash
288
289
  kairo setup --agents all
289
290
  kairo install --agents all
290
291
  ```
291
292
 
293
+ ### Pi runtime (auditable)
294
+
295
+ ```bash
296
+ kairo run --agent pi --task "Review this repository" --permissions read-only --follow
297
+ ```
298
+
299
+ Launches `pi --mode json --no-session` (optional `--model`). `read-only` maps to
300
+ `--tools read,grep,find,ls`; other permission aliases are rejected (never translated
301
+ to `--approve`). Custom `PI_CODING_AGENT_DIR` blocks config writes in 0.6.0 but does
302
+ not block runtime. Kairo does not install Pi or assert subscription/entitlement.
303
+
304
+ ### Bounded review (Codex / Pi)
305
+
306
+ Read-only native review against a Git snapshot. Never mutates the repo, never
307
+ auto-fixes, and never persists prompts, diffs, or transcripts. Receipts land under
308
+ `~/.harness/reviews/<reviewId>/receipt.json` (secret-free). Only Codex and Pi are
309
+ `reviewCompatible` in 0.7.0; Cursor / Claude / OpenCode report `false` until audited.
310
+
311
+ ```bash
312
+ kairo review --agent codex
313
+ kairo review --agent pi --base main
314
+ kairo review --agent codex --commit <sha>
315
+ kairo review --agent codex --fail-on high --json
316
+ kairo reviews list [--limit N] [--json]
317
+ kairo reviews show <reviewId> [--json]
318
+ ```
319
+
320
+ `--agent` is always required. Scope is working-tree by default; use `--base` or
321
+ `--commit` (mutually exclusive). Private paths need `--include-private` plus TTY
322
+ confirm or `--yes`/`--confirm`. Exit codes: `0` ok, `1` severity threshold,
323
+ `2` operational/stale/invalid/cancel.
324
+
325
+ Cockpit: **Runs → Reviews** lists receipts and opens a read-only detail view
326
+ (no launch from Cockpit in v1).
327
+
292
328
  Primary governance flow:
293
329
 
294
330
  ```txt
@@ -518,9 +554,11 @@ kairo components rollback engram-memory --receipt <id> --dry-run
518
554
 
519
555
  Without `--agents`, Kairo uses the intersection of detected agents and
520
556
  Kairo-managed Engram agents (`cursor`, `codex`, `opencode`, `claude` → setup slug
521
- `claude-code`). After setup, status is `restart_required` — restart the agent to
557
+ `claude-code`, `pi` → `pi`). After setup, status is `restart_required` — restart the agent to
522
558
  load MCP; configuration evidence is not runtime-active. Receipts live under
523
- `~/.harness/integrations/engram/`.
559
+ `~/.harness/integrations/engram/`. For Pi, positive evidence requires
560
+ `~/.pi/agent/settings.json` packages (`npm:gentle-engram`, `npm:pi-mcp-adapter`) and
561
+ `~/.pi/agent/mcp.json` with `mcpServers.engram`.
524
562
 
525
563
  ### SDD Core skills
526
564
 
@@ -537,7 +575,7 @@ same `sdd-core` apply path. Lifecycle keeps persona frozen (`preservePersona`):
537
575
  it never auto-activates teaching. Explicit persona changes stay on
538
576
  `kairo components configure sdd-core`.
539
577
 
540
- **Destinations.** Cursor, Codex, and OpenCode share `~/.agents/skills/<id>/`.
578
+ **Destinations.** Cursor, Codex, OpenCode, and Pi share `~/.agents/skills/<id>/`.
541
579
  Claude uses `~/.claude/skills/<id>/`. One physical tree per root; consumers are
542
580
  recorded per destination.
543
581
 
@@ -58,7 +58,8 @@
58
58
  "cursor": "- In repos with workspace harness: also read `.cursor/rules/` and `docs/specs/`.",
59
59
  "codex": "- In repos with workspace harness: also read `.codex/skills/sdd/` when present.",
60
60
  "claude": "- In repos with workspace harness: repo `AGENTS.md` wins over `CLAUDE.md` pointers.",
61
- "opencode": "- In repos with workspace harness: also read `.opencode/` SDD assets when present."
61
+ "opencode": "- In repos with workspace harness: also read `.opencode/` SDD assets when present.",
62
+ "pi": "- Skills load from `~/.agents/skills` (shared Agent Skills root). Teaching persona activates only via the managed section of `~/.pi/agent/AGENTS.md`."
62
63
  }
63
64
  },
64
65
  {
@@ -80,7 +81,8 @@
80
81
  "cursor": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup cursor`).",
81
82
  "codex": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup codex`).",
82
83
  "claude": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup claude-code`).",
83
- "opencode": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup opencode`)."
84
+ "opencode": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup opencode`).",
85
+ "pi": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup pi`). Evidence: `~/.pi/agent/settings.json` packages + `mcp.json` engram server. Restart Pi after setup; Kairo never runs `engram doctor`/`serve` or touches `engram.db`."
84
86
  }
85
87
  },
86
88
  {
@@ -5,7 +5,7 @@
5
5
  ## Role
6
6
 
7
7
  The Harness Orchestrator (conductor) coordinates work across local coding agents
8
- (Cursor, Codex, OpenCode, Claude). It does not replace any agent. It defines how
8
+ (Cursor, Codex, OpenCode, Claude, Pi). It does not replace any agent. It defines how
9
9
  agents hand off work and which source of truth governs.
10
10
 
11
11
  ## Authority order
@@ -5,7 +5,7 @@
5
5
  ## Role
6
6
 
7
7
  The Harness Orchestrator (conductor) coordinates work across local coding agents
8
- (Cursor, Codex, OpenCode, Claude). It does not replace any agent. It defines how
8
+ (Cursor, Codex, OpenCode, Claude, Pi). It does not replace any agent. It defines how
9
9
  agents hand off work and which source of truth governs.
10
10
 
11
11
  ## Authority order
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.5.1",
4
- "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Gemini, Copilot, Engram, and Graphify.",
3
+ "version": "0.7.0",
4
+ "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
7
7
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "agents",
46
46
  "codex",
47
47
  "cursor",
48
+ "pi",
48
49
  "kairo",
49
50
  "kairo-runtime",
50
51
  "harness",
@@ -26,7 +26,7 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
26
26
  const require = createRequire(import.meta.url);
27
27
  const pkg = require(join(root, "package.json"));
28
28
 
29
- assert.equal(pkg.version, "0.5.0");
29
+ assert.equal(pkg.version, "0.7.0");
30
30
  assert.ok(pkg.dependencies["ansi-escapes"]);
31
31
 
32
32
  assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
@@ -4,7 +4,7 @@
4
4
  # Safe by design:
5
5
  # - no sudo
6
6
  # - no shell profile changes
7
- # - does not install Cursor/Codex/OpenCode/Claude
7
+ # - does not install Cursor/Codex/OpenCode/Claude/Pi
8
8
  # - default ends with setup --dry-run (no writes)
9
9
  # - explicit apply: pass --yes to run setup --yes
10
10
  #
@@ -88,9 +88,9 @@ if (!payload.ok || payload.overall !== 'ok') {
88
88
  process.exit(1);
89
89
  }
90
90
  const managed = payload.agents.filter((agent) => agent.managed).map((agent) => agent.id).sort();
91
- const expected = ['claude', 'codex', 'cursor', 'opencode'];
91
+ const expected = ['claude', 'codex', 'cursor', 'opencode', 'pi'];
92
92
  if (JSON.stringify(managed) !== JSON.stringify(expected)) {
93
- console.error('Expected all four agents managed');
93
+ console.error('Expected all five agents managed');
94
94
  process.exit(1);
95
95
  }
96
96
  " "$status_json"
@@ -105,7 +105,8 @@ const targets = [
105
105
  '.cursor/AGENTS.md',
106
106
  '.codex/AGENTS.md',
107
107
  '.config/opencode/AGENTS.md',
108
- '.claude/CLAUDE.md'
108
+ '.claude/CLAUDE.md',
109
+ '.pi/agent/AGENTS.md'
109
110
  ];
110
111
 
111
112
  for (const relativePath of targets) {
@@ -143,7 +144,8 @@ mkdir -p \
143
144
  "$FAKE_HOME/.cursor" \
144
145
  "$FAKE_HOME/.codex" \
145
146
  "$FAKE_HOME/.config/opencode" \
146
- "$FAKE_HOME/.claude"
147
+ "$FAKE_HOME/.claude" \
148
+ "$FAKE_HOME/.pi/agent"
147
149
 
148
150
  echo
149
151
  echo "== curl install.sh | sh --version ${VERSION} --yes --agents all =="
package/src/cli.js CHANGED
@@ -38,6 +38,7 @@ import { runWorkspaceDetect, runWorkspaceDoctor, runWorkspaceInit, runWorkspaceU
38
38
  import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orchestrator.js";
39
39
  import { runIntelligenceCli } from "./global/intelligence-cli.js";
40
40
  import { runGlobalRun, runGlobalRuns } from "./global/runtime/run-cli.js";
41
+ import { runGlobalReview, runGlobalReviews } from "./global/runtime/review/review-cli.js";
41
42
  import {
42
43
  LEGACY_PACKAGE_NAME,
43
44
  PACKAGE_NAME,
@@ -111,6 +112,12 @@ export async function runCli(argv) {
111
112
  case "runs":
112
113
  await runGlobalRuns(optionsWithPolicy, packageManifest);
113
114
  return;
115
+ case "review":
116
+ await runGlobalReview(optionsWithPolicy, packageManifest);
117
+ return;
118
+ case "reviews":
119
+ await runGlobalReviews(optionsWithPolicy, packageManifest);
120
+ return;
114
121
  case "intelligence":
115
122
  await runIntelligenceCli(optionsWithPolicy, packageManifest);
116
123
  return;
@@ -371,6 +378,11 @@ export function parseArgs(argv) {
371
378
  intelligencePaths: [],
372
379
  runsAction: null,
373
380
  runId: null,
381
+ reviewId: null,
382
+ reviewsAction: null,
383
+ base: null,
384
+ commit: null,
385
+ failOn: null,
374
386
  agent: null,
375
387
  task: null,
376
388
  model: null,
@@ -401,6 +413,10 @@ export function parseArgs(argv) {
401
413
  parseRunsAction(args, options);
402
414
  }
403
415
 
416
+ if (command === "reviews") {
417
+ parseReviewsAction(args, options);
418
+ }
419
+
404
420
  if (command === "intelligence") {
405
421
  parseIntelligenceAction(args, options);
406
422
  }
@@ -495,6 +511,15 @@ export function parseArgs(argv) {
495
511
  else if (arg.startsWith("--timeout=")) options.timeoutMs = parsePositiveInt(arg.slice("--timeout=".length), "timeout") * 1000;
496
512
  else if (arg === "--include-private") options.includePrivate = true;
497
513
  else if (arg === "--cloud-consent") options.cloudConsent = true;
514
+ else if (arg === "--base") options.base = requireFlagValue("--base", args[++index]);
515
+ else if (arg.startsWith("--base=")) options.base = requireFlagValue("--base", arg.slice("--base=".length));
516
+ else if (arg === "--commit") options.commit = requireFlagValue("--commit", args[++index]);
517
+ else if (arg.startsWith("--commit=")) {
518
+ options.commit = requireFlagValue("--commit", arg.slice("--commit=".length));
519
+ } else if (arg === "--fail-on") options.failOn = requireFlagValue("--fail-on", args[++index]);
520
+ else if (arg.startsWith("--fail-on=")) {
521
+ options.failOn = requireFlagValue("--fail-on", arg.slice("--fail-on=".length));
522
+ }
498
523
  else if (arg === "--help" || arg === "-h") options.help = true;
499
524
  else if (arg === "--version" || arg === "-v") options.version = true;
500
525
  else throw new Error(`Unknown option "${arg}".`);
@@ -624,6 +649,26 @@ function parseRunsAction(args, options) {
624
649
  }
625
650
  }
626
651
 
652
+ function parseReviewsAction(args, options) {
653
+ const action = args[0];
654
+ if (!action || action.startsWith("-")) {
655
+ options.reviewsAction = "list";
656
+ return;
657
+ }
658
+ if (!new Set(["list", "show"]).has(action)) {
659
+ throw new Error(`Unknown reviews action "${action}". Use list or show.`);
660
+ }
661
+ args.shift();
662
+ options.reviewsAction = action;
663
+ if (action === "show") {
664
+ const reviewId = args[0];
665
+ if (!reviewId || reviewId.startsWith("-")) {
666
+ throw new Error(`Missing review id. Use: ${formatCliCommand("reviews show <reviewId>")}`);
667
+ }
668
+ options.reviewId = args.shift();
669
+ }
670
+ }
671
+
627
672
  function parseIntelligenceAction(args, options) {
628
673
  const action = args[0];
629
674
  if (!action || action.startsWith("-")) {
@@ -657,6 +702,13 @@ function parsePositiveInt(value, label) {
657
702
  return parsed;
658
703
  }
659
704
 
705
+ function requireFlagValue(flag, value) {
706
+ if (value == null || value === "" || String(value).startsWith("-")) {
707
+ throw new Error(`Missing value for ${flag}.`);
708
+ }
709
+ return value;
710
+ }
711
+
660
712
  function parseScope(value) {
661
713
  if (!SCOPES.has(value)) {
662
714
  throw new Error(`Invalid scope "${value}". Use agent-global or workspace.`);
@@ -673,6 +725,8 @@ function normalizeCommand(command) {
673
725
  if (command === "orchestrator") return "orchestrator";
674
726
  if (command === "run") return "run";
675
727
  if (command === "runs") return "runs";
728
+ if (command === "review") return "review";
729
+ if (command === "reviews") return "reviews";
676
730
  if (command === "intelligence" || command === "intel") return "intelligence";
677
731
  if (command === "setup") return "setup";
678
732
  if (command === "status") return "status";
@@ -732,7 +786,7 @@ function printHelp() {
732
786
  console.log(`${BRAND.displayName} (${PACKAGE_NAME})
733
787
 
734
788
  ${BRAND.tagline}. ${BRAND.displayName} does not install AI apps — it powers and
735
- coordinates agents you already have (Cursor, Codex, OpenCode, Claude) with managed
789
+ coordinates agents you already have (Cursor, Codex, OpenCode, Claude, Pi) with managed
736
790
  sections, components, backups, and drift repair under ~/.harness.
737
791
 
738
792
  Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
@@ -747,6 +801,9 @@ Usage:
747
801
  ${cli} runs list [--json] [--limit <n>] [--active-only]
748
802
  ${cli} runs show <runId> [--json] [--limit <n>] [--follow]
749
803
  ${cli} runs stop <runId> [--json]
804
+ ${cli} review --agent codex|pi [--base <ref>|--commit <sha>] [--model <name>] [--include-private] [--yes|--confirm] [--fail-on high|medium|low] [--json]
805
+ ${cli} reviews list [--limit <n>] [--json]
806
+ ${cli} reviews show <reviewId> [--json]
750
807
  ${cli} orchestrator [--json] Read-only agent capability diagnostics
751
808
  ${cli} intelligence [status|models|context|route|ask] [--json]
752
809
  ${cli} intelligence models --backend opencode-go|opencode-zen|opencode
@@ -795,6 +852,8 @@ Commands:
795
852
  Tab switches region only when content is interactive (runs/launch).
796
853
  run Launch a managed agent run with local audit trail.
797
854
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
855
+ review Bounded read-only review via Codex or Pi; receipts under ~/.harness/reviews/.
856
+ reviews List or show prior review receipts (secret-free).
798
857
  orchestrator Read-only capability registry diagnostics (--json supported).
799
858
  intelligence Harness Engineering layer: backends, context packs, routing, budgets.
800
859
  Local-first (Ollama). Cloud only with --cloud-consent.
@@ -0,0 +1,53 @@
1
+ import { resolve } from "node:path";
2
+ import { createManagedConfigAdapter } from "../managed-config-adapter.js";
3
+
4
+ export const PI_DEFAULT_ROOT_DIR = ".pi/agent";
5
+ export const PI_DEFAULT_CONFIG_FILE = ".pi/agent/AGENTS.md";
6
+ export const PI_CODING_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
7
+
8
+ const CUSTOM_DIR_MESSAGE =
9
+ `Pi config writes are unsupported when ${PI_CODING_AGENT_DIR_ENV} points away from `
10
+ + `~/${PI_DEFAULT_ROOT_DIR} (out of scope for Kairo 0.6.0). `
11
+ + `Unset ${PI_CODING_AGENT_DIR_ENV} to manage the default directory; runtime launches remain available.`;
12
+
13
+ /** True when PI_CODING_AGENT_DIR is set to a non-default absolute/relative path. */
14
+ export function isCustomPiCodingAgentDir(env = process.env, homeDir) {
15
+ const raw = env?.[PI_CODING_AGENT_DIR_ENV];
16
+ if (raw == null || String(raw).trim() === "") return false;
17
+ if (homeDir == null) return true;
18
+ return resolve(String(raw).trim()) !== resolve(homeDir, PI_DEFAULT_ROOT_DIR);
19
+ }
20
+
21
+ export function assertDefaultPiConfigDir(context = {}) {
22
+ const env = context.env ?? process.env;
23
+ if (isCustomPiCodingAgentDir(env, context.homeDir)) {
24
+ throw new Error(CUSTOM_DIR_MESSAGE);
25
+ }
26
+ }
27
+
28
+ const managed = createManagedConfigAdapter({
29
+ id: "pi",
30
+ label: "Pi",
31
+ rootDir: PI_DEFAULT_ROOT_DIR,
32
+ configFile: PI_DEFAULT_CONFIG_FILE
33
+ });
34
+
35
+ /** Managed adapter for ~/.pi/agent/AGENTS.md; custom PI_CODING_AGENT_DIR fails before writes. */
36
+ export default {
37
+ ...managed,
38
+
39
+ plan(context) {
40
+ assertDefaultPiConfigDir(context);
41
+ return managed.plan(context);
42
+ },
43
+
44
+ async apply(context, plan) {
45
+ assertDefaultPiConfigDir(context);
46
+ return managed.apply(context, plan);
47
+ },
48
+
49
+ async uninstall(context, stateEntry) {
50
+ assertDefaultPiConfigDir(context);
51
+ return managed.uninstall(context, stateEntry);
52
+ }
53
+ };
@@ -2,6 +2,7 @@ import cursorManaged from "../adapters/cursor.js";
2
2
  import codexManaged from "../adapters/codex.js";
3
3
  import opencodeManaged from "../adapters/opencode.js";
4
4
  import claudeManaged from "../adapters/claude.js";
5
+ import piManaged from "../adapters/pi.js";
5
6
  import { createAgentCapabilityAdapter } from "./create-capability-adapter.js";
6
7
 
7
8
  const cursor = createAgentCapabilityAdapter({
@@ -37,7 +38,16 @@ const claude = createAgentCapabilityAdapter({
37
38
  runExecutable: "claude"
38
39
  });
39
40
 
40
- const CAPABILITY_ADAPTERS = [cursor, codex, opencode, claude];
41
+ const pi = createAgentCapabilityAdapter({
42
+ id: "pi",
43
+ label: "Pi",
44
+ managedAdapter: piManaged,
45
+ executable: "pi",
46
+ opaqueAuth: true,
47
+ runExecutable: "pi"
48
+ });
49
+
50
+ const CAPABILITY_ADAPTERS = [cursor, codex, opencode, claude, pi];
41
51
 
42
52
  export const AGENT_CAPABILITY_IDS = CAPABILITY_ADAPTERS.map((adapter) => adapter.id);
43
53
 
@@ -30,14 +30,16 @@ export const AGENT_LABELS = {
30
30
  cursor: "Cursor",
31
31
  codex: "Codex",
32
32
  opencode: "OpenCode",
33
- claude: "Claude Code"
33
+ claude: "Claude Code",
34
+ pi: "Pi"
34
35
  };
35
36
 
36
37
  export const AGENT_SYMBOLS = {
37
38
  cursor: "◆",
38
39
  codex: "◆",
39
40
  opencode: "◆",
40
- claude: "◆"
41
+ claude: "◆",
42
+ pi: "◆"
41
43
  };
42
44
 
43
45
  export const TOKENS = {
@@ -150,7 +150,7 @@ export function resolveDashboardRecommendation({
150
150
  kind: NEXT_STEP_KINDS.LAUNCH,
151
151
  title: "Create a new run",
152
152
  message: "Create a new run",
153
- detail: "Delegate a task to Cursor, Codex or Claude.",
153
+ detail: "Delegate a task to Cursor, Codex, Claude, or Pi.",
154
154
  targetView: RECOMMENDATION_TARGETS.LAUNCH,
155
155
  targetAction: "launch"
156
156
  };
@@ -734,7 +734,7 @@ function printAdapterMatrixReport(report) {
734
734
  console.log(commandHeader("adapters — supported agent integrations"));
735
735
  console.log(`Home: ${report.homeDir}`);
736
736
  console.log("");
737
- console.log(`${BRAND.displayName} does not install Cursor, Codex, OpenCode, or Claude Code.`);
737
+ console.log(`${BRAND.displayName} does not install Cursor, Codex, OpenCode, Claude Code, or Pi.`);
738
738
  console.log("It configures managed sections in each agent's config files.");
739
739
  console.log("");
740
740
  console.log(`Supported: ${report.supportedCount} Detected: ${report.detectedCount} Managed: ${report.managedCount}`);
@@ -144,6 +144,16 @@ export function reduceCockpitUi(state, action) {
144
144
  returnView: null
145
145
  };
146
146
  }
147
+ if (state.view === ORCHESTRATOR_VIEWS.REVIEW_DETAIL) {
148
+ const back = ORCHESTRATOR_VIEWS.REVIEWS;
149
+ return {
150
+ ...state,
151
+ view: back,
152
+ listIndex: 0,
153
+ region: defaultRegionForView(back, state.layoutMode),
154
+ returnView: null
155
+ };
156
+ }
147
157
  if (isRunsChildView(state.view)) {
148
158
  return goRunsHub(state);
149
159
  }
@@ -18,6 +18,8 @@ const CONTENT_INTERACTIVE_VIEWS = new Set([
18
18
  ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
19
19
  ORCHESTRATOR_VIEWS.RECENT_RUNS,
20
20
  ORCHESTRATOR_VIEWS.RUN_DETAIL,
21
+ ORCHESTRATOR_VIEWS.REVIEWS,
22
+ ORCHESTRATOR_VIEWS.REVIEW_DETAIL,
21
23
  ORCHESTRATOR_VIEWS.LAUNCH,
22
24
  ORCHESTRATOR_VIEWS.ACTIVITY
23
25
  ]);
@@ -41,7 +43,11 @@ export function defaultRegionForView(view, layoutMode = LAYOUT_MODES.COMPACT) {
41
43
 
42
44
  export function interactiveRegionsFor(state) {
43
45
  const regions = regionsForLayout(state.layoutMode);
44
- if (!isContentInteractiveView(state.view) || state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
46
+ if (
47
+ !isContentInteractiveView(state.view)
48
+ || state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL
49
+ || state.view === ORCHESTRATOR_VIEWS.REVIEW_DETAIL
50
+ ) {
45
51
  return regions.filter((region) => region === COCKPIT_REGIONS.NAV);
46
52
  }
47
53
  return regions.filter(
@@ -51,7 +57,12 @@ export function interactiveRegionsFor(state) {
51
57
 
52
58
  export function canTabBetweenRegions(state) {
53
59
  if (!isContentInteractiveView(state.view)) return false;
54
- if (state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) return false;
60
+ if (
61
+ state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL
62
+ || state.view === ORCHESTRATOR_VIEWS.REVIEW_DETAIL
63
+ ) {
64
+ return false;
65
+ }
55
66
  return interactiveRegionsFor(state).length >= 2;
56
67
  }
57
68
 
@@ -70,7 +81,11 @@ export function routeCockpitKey(state, keyAction) {
70
81
  return { type: "arrow", direction: keyAction.direction };
71
82
  }
72
83
  if (state.region === COCKPIT_REGIONS.CONTENT && isContentInteractiveView(state.view)) {
73
- if (state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL || state.view === ORCHESTRATOR_VIEWS.LAUNCH) {
84
+ if (
85
+ state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL
86
+ || state.view === ORCHESTRATOR_VIEWS.REVIEW_DETAIL
87
+ || state.view === ORCHESTRATOR_VIEWS.LAUNCH
88
+ ) {
74
89
  return null;
75
90
  }
76
91
  return {
@@ -250,6 +250,11 @@ export function buildFooterModel({
250
250
  return { text: parts.join(` ${glyphs.bullet} `) };
251
251
  }
252
252
 
253
+ if (view === ORCHESTRATOR_VIEWS.REVIEW_DETAIL) {
254
+ parts.push("Esc Back");
255
+ return { text: parts.join(` ${glyphs.bullet} `) };
256
+ }
257
+
253
258
  if (view === ORCHESTRATOR_VIEWS.CHANGES) {
254
259
  return { text: buildChangesFooterParts(changesPhase).join(` ${glyphs.bullet} `) };
255
260
  }
@@ -263,6 +268,7 @@ export function buildFooterModel({
263
268
  const showTab = view === ORCHESTRATOR_VIEWS.RUNS
264
269
  || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
265
270
  || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
271
+ || view === ORCHESTRATOR_VIEWS.REVIEWS
266
272
  || view === ORCHESTRATOR_VIEWS.LAUNCH
267
273
  || view === ORCHESTRATOR_VIEWS.ACTIVITY;
268
274
  if (showTab) {
@@ -280,7 +286,8 @@ export function buildFooterModel({
280
286
  || region === COCKPIT_REGIONS.NAV
281
287
  || view === ORCHESTRATOR_VIEWS.RUNS
282
288
  || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
283
- || view === ORCHESTRATOR_VIEWS.RECENT_RUNS) {
289
+ || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
290
+ || view === ORCHESTRATOR_VIEWS.REVIEWS) {
284
291
  parts.push("Enter Open");
285
292
  }
286
293
  if (view !== ORCHESTRATOR_VIEWS.LAUNCH) {
@@ -0,0 +1,62 @@
1
+ /** Read-only Cockpit helpers for review receipts under ~/.harness/reviews/. */
2
+
3
+ export function countFindingsBySeverity(findings = []) {
4
+ const counts = { high: 0, medium: 0, low: 0 };
5
+ for (const finding of findings) {
6
+ const severity = String(finding?.severity ?? "").toLowerCase();
7
+ if (counts[severity] != null) counts[severity] += 1;
8
+ }
9
+ return counts;
10
+ }
11
+
12
+ export function selectReviewFromList(receipts = [], listIndex = 0) {
13
+ if (!Array.isArray(receipts) || receipts.length === 0) return null;
14
+ const index = Math.min(Math.max(0, listIndex), receipts.length - 1);
15
+ return receipts[index] ?? null;
16
+ }
17
+
18
+ export function formatReviewListLines(receipts = []) {
19
+ if (!Array.isArray(receipts) || receipts.length === 0) {
20
+ return ["No review receipts yet. Run kairo review --agent codex|pi."];
21
+ }
22
+ return receipts.map((receipt) => {
23
+ const counts = countFindingsBySeverity(receipt.findings);
24
+ const findingTotal = (receipt.findings ?? []).length;
25
+ const created = String(receipt.createdAt ?? "").slice(0, 19).replace("T", " ");
26
+ return `${receipt.reviewId} ${String(receipt.state).padEnd(10)} ${String(receipt.agentId).padEnd(6)} ${findingTotal}f (h${counts.high}/m${counts.medium}/l${counts.low}) ${created}`;
27
+ });
28
+ }
29
+
30
+ export function formatReviewDetailLines(receipt) {
31
+ if (!receipt) return ["Review receipt not found."];
32
+ const counts = countFindingsBySeverity(receipt.findings);
33
+ const snapshot = receipt.snapshot ?? {};
34
+ const totals = snapshot.totals ?? {};
35
+ const lines = [
36
+ `Id: ${receipt.reviewId}`,
37
+ `Agent: ${receipt.agentId}${receipt.model ? ` · model ${receipt.model}` : ""}`,
38
+ `State: ${receipt.state}`,
39
+ `Created: ${receipt.createdAt ?? "n/a"}`,
40
+ `Findings: ${(receipt.findings ?? []).length} (high ${counts.high}, medium ${counts.medium}, low ${counts.low})`,
41
+ `Snapshot: ${snapshot.mode ?? "n/a"} · files ${totals.fileCount ?? 0} · fingerprint ${String(snapshot.fingerprint ?? "").slice(0, 12) || "n/a"}`
42
+ ];
43
+ if ((receipt.warnings ?? []).length > 0) {
44
+ lines.push(`Warnings: ${receipt.warnings.length}`);
45
+ }
46
+ const findings = receipt.findings ?? [];
47
+ if (findings.length === 0) {
48
+ lines.push("", "No findings recorded.");
49
+ return lines;
50
+ }
51
+ lines.push("", "Findings (read-only):");
52
+ for (const finding of findings.slice(0, 20)) {
53
+ const loc = finding.line != null ? `:${finding.line}` : "";
54
+ lines.push(
55
+ ` [${String(finding.severity).toUpperCase()}] ${finding.path ?? "?"}${loc} — ${finding.title ?? "untitled"}`
56
+ );
57
+ }
58
+ if (findings.length > 20) {
59
+ lines.push(` … ${findings.length - 20} more (use kairo reviews show ${receipt.reviewId})`);
60
+ }
61
+ return lines;
62
+ }
@@ -13,6 +13,12 @@ export const RUNS_HUB_ITEMS = [
13
13
  view: ORCHESTRATOR_VIEWS.RECENT_RUNS,
14
14
  description: "Review completed and failed run outcomes."
15
15
  },
16
+ {
17
+ id: "reviews",
18
+ label: "Reviews",
19
+ view: ORCHESTRATOR_VIEWS.REVIEWS,
20
+ description: "Browse secret-free bounded-review receipts (read-only)."
21
+ },
16
22
  {
17
23
  id: "launch",
18
24
  label: "New run",
@@ -27,13 +33,16 @@ export function isRunsBranchView(view) {
27
33
  || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
28
34
  || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
29
35
  || view === ORCHESTRATOR_VIEWS.LAUNCH
30
- || view === ORCHESTRATOR_VIEWS.RUN_DETAIL;
36
+ || view === ORCHESTRATOR_VIEWS.RUN_DETAIL
37
+ || view === ORCHESTRATOR_VIEWS.REVIEWS
38
+ || view === ORCHESTRATOR_VIEWS.REVIEW_DETAIL;
31
39
  }
32
40
 
33
41
  export function isRunsChildView(view) {
34
42
  return view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
35
43
  || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
36
- || view === ORCHESTRATOR_VIEWS.LAUNCH;
44
+ || view === ORCHESTRATOR_VIEWS.LAUNCH
45
+ || view === ORCHESTRATOR_VIEWS.REVIEWS;
37
46
  }
38
47
 
39
48
  export function resolveRunsHubItem(listIndex = 0, items = RUNS_HUB_ITEMS) {