@kal-elsam/kairo-runtime 0.16.0 → 0.18.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 (81) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/package.json +2 -1
  3. package/scripts/cockpit-smoke.mjs +1 -1
  4. package/scripts/ux-smoke-test.sh +3 -3
  5. package/src/cli.js +96 -11
  6. package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
  7. package/src/global/architect/architect-cli.js +76 -0
  8. package/src/global/architect/architect-codex.js +146 -0
  9. package/src/global/architect/architect-manager.js +125 -0
  10. package/src/global/architect/architect-store.js +377 -0
  11. package/src/global/architect/architect-types.js +47 -0
  12. package/src/global/cli-help.js +10 -1
  13. package/src/global/cockpit/app.js +493 -0
  14. package/src/global/cockpit/card.js +111 -0
  15. package/src/global/cockpit/cli.js +33 -0
  16. package/src/global/cockpit/gauge.js +31 -0
  17. package/src/global/cockpit/project-overlay.js +693 -0
  18. package/src/global/cockpit/rows.js +148 -0
  19. package/src/global/cockpit/theme.js +118 -0
  20. package/src/global/cockpit/view.js +1298 -0
  21. package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
  22. package/src/global/conversation/cli.js +53 -0
  23. package/src/global/conversation/codex-sandbox.js +230 -0
  24. package/src/global/conversation/cursor-sandbox.js +215 -0
  25. package/src/global/conversation/project-analysis.js +204 -0
  26. package/src/global/conversation/project-profile.js +178 -0
  27. package/src/global/conversation/project-router.js +149 -0
  28. package/src/global/conversation/project-strategy-store.js +64 -0
  29. package/src/global/conversation/project-strategy.js +514 -0
  30. package/src/global/conversation/sanitized-snapshot.js +169 -0
  31. package/src/global/conversation/secret-scanner.js +71 -0
  32. package/src/global/conversation/service.js +1090 -0
  33. package/src/global/conversation/session-store.js +75 -0
  34. package/src/global/conversation/transcript-store.js +79 -0
  35. package/src/global/conversation/ui.js +195 -0
  36. package/src/global/intelligence/capability-scoring.js +480 -0
  37. package/src/global/intelligence/execution-router.js +466 -0
  38. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  39. package/src/global/intelligence/kairobench-runner.js +85 -0
  40. package/src/global/intelligence/kairobench-source.js +34 -0
  41. package/src/global/intelligence/kairobench-tasks.js +47 -0
  42. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  43. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  44. package/src/global/intelligence/model-capability-registry.js +125 -0
  45. package/src/global/intelligence/model-intelligence.js +1646 -0
  46. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  47. package/src/global/intelligence/quick-ask.js +149 -0
  48. package/src/global/intelligence/role-profiles.js +251 -0
  49. package/src/global/intelligence/skill-catalog.js +67 -0
  50. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  51. package/src/global/mcp/kairo-mcp.js +51 -18
  52. package/src/global/mcp/work-snapshot-rule.js +4 -2
  53. package/src/global/mcp/workspace-binding.js +88 -0
  54. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  55. package/src/global/mcp-install.js +8 -1
  56. package/src/global/observability/artificial-analysis-models.js +118 -0
  57. package/src/global/observability/claude-models.js +31 -0
  58. package/src/global/observability/claude-usage.js +112 -0
  59. package/src/global/observability/codex-models.js +96 -0
  60. package/src/global/observability/codex-usage.js +160 -0
  61. package/src/global/observability/cursor-auth.js +88 -0
  62. package/src/global/observability/cursor-models.js +101 -0
  63. package/src/global/observability/huggingface-leaderboard.js +97 -0
  64. package/src/global/observability/opencode-models.js +101 -0
  65. package/src/global/observability/opencode-usage.js +162 -0
  66. package/src/global/paths.js +49 -2
  67. package/src/global/profile.js +23 -1
  68. package/src/global/runtime/execution-adapters/claude.js +63 -30
  69. package/src/global/runtime/execution-adapters/codex.js +9 -2
  70. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  71. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  72. package/src/global/runtime/execution-worktree-manager.js +924 -0
  73. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  74. package/src/global/runtime/execution-worktree-store.js +83 -0
  75. package/src/global/runtime/execution-worktree-types.js +45 -0
  76. package/src/global/runtime/run-events.js +38 -0
  77. package/src/global/runtime/run-manager.js +22 -6
  78. package/src/global/runtime/run-supervisor.js +41 -12
  79. package/src/global/runtime/usage-manager.js +96 -0
  80. package/src/global/runtime/usage-store.js +69 -0
  81. package/src/global/runtime/usage-types.js +62 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,85 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.18.0 — 2026-09-18 (Kairo Runtime)
9
+
10
+ Minor release. Provider visibility and quota-aware improvements to the
11
+ `/project` PROJECT TEAM view.
12
+
13
+ ### Added
14
+
15
+ - The PROJECT TEAM view (dashboard panel and `/project` overlay) now
16
+ shows the real provider alongside each role's model
17
+ (`CockpitView.teamRoleLabel()`), so it's clear which subscription
18
+ actually serves each role.
19
+ - `/project cursor exhausted|available` — a manual, human-reported
20
+ toggle for Cursor's quota state, persisted through the same
21
+ per-provider usage store Codex/Claude/OpenCode Go already use. Cursor
22
+ exposes no real, zero-cost local usage/billing read, so this is the
23
+ one honest signal Kairo can act on; an exhausted Cursor is excluded
24
+ from team recommendations until cleared.
25
+ - An early "LOW" warning tag (`LOW_QUOTA_WARN_PERCENT`, 20% remaining)
26
+ for Codex/Claude/OpenCode Go quota windows, shown in the compact usage
27
+ bar and `/usage`, strictly before the existing 5% hard-exclusion
28
+ threshold takes effect.
29
+
30
+ ### Fixed
31
+
32
+ - The `/project` overlay's modal frame now uses a distinct,
33
+ high-contrast border instead of sharing the dashboard card's
34
+ low-contrast outline, and compacted its per-line spacing so a full
35
+ six-role team stays within the overlay's height budget.
36
+
37
+ ## 0.17.0 — 2026-09-17 (Kairo Runtime)
38
+
39
+ Minor release. Execution-worktree isolation (real git worktrees, per-role
40
+ commit transactions, preview + `git merge --ff-only` apply, cancellation
41
+ and crash recovery), a per-provider budget/usage state machine, automatic
42
+ Builder→Debugger→Tester orchestration, and several real `/project`
43
+ cockpit UX fixes.
44
+
45
+ ### Added
46
+
47
+ - Execution-worktree isolation (`execution-worktree-manager.js`): real
48
+ `git worktree add`-backed isolation per task, a per-role commit
49
+ transaction (`beginRoleRun`/`completeRoleRun`) that only ever commits
50
+ a role's own validated diff (Kairo owns every real commit, never the
51
+ agent), `previewWorktreeMerge`/`applyWorktreeMerge` (confirmation
52
+ bound to an exact `{baseSha, finalHeadSha, fingerprint}`, applied only
53
+ via `git merge --ff-only`), and `cancelWorktree`/`reconcileWorktrees`/
54
+ `discardWorktree` for cancellation and crash recovery.
55
+ - `controlledHeadSha`: every state-changing boundary (not just ACTIVE)
56
+ verifies the worktree's real HEAD against the last commit Kairo itself
57
+ produced, closing a rogue-commit bypass reachable while a worktree sat
58
+ idle in PENDING.
59
+ - Per-provider budget/usage state machine (`usage-manager.js`):
60
+ cumulative token/cost tracking per provider, a
61
+ HEALTHY/MODERATE/CONSERVE/CRITICAL/EXHAUSTED classification against a
62
+ configurable `profile.providerTokenBudgets`, and one real enforcement
63
+ point — a new run is refused once its provider is EXHAUSTED.
64
+ - `runOrchestratedChain` (`execution-worktree-orchestrator.js`):
65
+ automatic Builder→Debugger→Tester chaining inside one execution
66
+ worktree, stopping immediately on any real failure. Injects matching
67
+ real project skills (`docs/skills`, `.claude/skills`, etc. — the same
68
+ catalog ASK-mode routing already reads) into each role's task by
69
+ name/path, never their body content.
70
+ - `/project` cockpit overlay: the PROJECT TEAM panel now shows the real
71
+ decision reason behind each role's model, a real bordered panel, the
72
+ cockpit's own live spinner during every in-flight action, and real
73
+ mouse support (clicks now reach the analyst/role picker lists).
74
+
75
+ ### Fixed
76
+
77
+ - Dashboard and `/project status` showed `qualityTeam` (comparative
78
+ reference) under a "PROJECT TEAM" title while the interactive overlay
79
+ showed the real operational `projectTeam` — both now read the same
80
+ real operational team.
81
+ - MCP snapshot writes require `--workspace-bound` plus an absolute `--cwd`
82
+ that matches `process.cwd()` or the unique `WORKSPACE_FOLDER_PATHS` folder.
83
+ Bound servers expose only `kairo_publish_work_snapshot`; the Cursor
84
+ extension ships that runtime as `dist/kairo-workspace.cjs` and spawns
85
+ absolute Node ≥20 — never PATH `kairo`. See [MCP](docs/mcp.md).
86
+
8
87
  ## 0.16.0 — 2026-08-13 (Kairo Runtime)
9
88
 
10
89
  Minor release. Public `kairo control-plane` command and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
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",
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@clack/prompts": "^1.7.0",
71
+ "@earendil-works/pi-tui": "0.85.1",
71
72
  "@modelcontextprotocol/server": "2.0.0",
72
73
  "ansi-escapes": "^7.3.0",
73
74
  "ink": "^5.2.1",
@@ -121,7 +121,7 @@ function smokeNavigation(layoutMode) {
121
121
  region: COCKPIT_REGIONS.CONTENT,
122
122
  unicode: false
123
123
  });
124
- assert.match(footer.text, /Tab/);
124
+ assert.equal(footer.text, "1·2 Select - Enter Run - ? Help - Esc Exit");
125
125
 
126
126
  state = applyKey(state, { type: "escape" });
127
127
  assert.equal(state.view, ORCHESTRATOR_VIEWS.HOME);
@@ -76,9 +76,9 @@ echo "Kairo Runtime UX smoke — capturing terminal output to $CAPTURE_DIR"
76
76
  echo
77
77
 
78
78
  run_capture help help
79
- assert_contains "$CAPTURE_DIR/help.txt" "JSON output (--json on supported commands):"
80
- assert_contains "$CAPTURE_DIR/help.txt" "report"
81
- assert_contains "$CAPTURE_DIR/help.txt" "More examples: README.md"
79
+ assert_contains "$CAPTURE_DIR/help.txt" "Usage:"
80
+ assert_contains "$CAPTURE_DIR/help.txt" "kairo help --all"
81
+ assert_contains "$CAPTURE_DIR/help.txt" "Docs: README.md · docs/cli-reference.md"
82
82
  assert_not_contains "$CAPTURE_DIR/help.txt" "Stable fields: ok, overall"
83
83
  assert_exit help 0
84
84
 
package/src/cli.js CHANGED
@@ -41,6 +41,10 @@ import { runWorkspaceDetect, runWorkspaceDoctor, runWorkspaceInit, runWorkspaceU
41
41
  import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orchestrator.js";
42
42
  import { runIntelligenceCli } from "./global/intelligence-cli.js";
43
43
  import { runGlobalRun, runGlobalRuns } from "./global/runtime/run-cli.js";
44
+ import { runArchitectCli, runPlansCli } from "./global/architect/architect-cli.js";
45
+ import { runConversationCli } from "./global/conversation/cli.js";
46
+ import { runUiCli } from "./global/conversation/ui.js";
47
+ import { runCockpitCli } from "./global/cockpit/cli.js";
44
48
  import { runGlobalReview, runGlobalReviews } from "./global/runtime/review/review-cli.js";
45
49
  import { runGlobalMonitor } from "./global/runtime/monitor/monitor-cli.js";
46
50
  import { runGlobalAlerts } from "./global/runtime/alerts/alert-cli.js";
@@ -116,6 +120,21 @@ export async function runCli(argv) {
116
120
  case "runs":
117
121
  await runGlobalRuns(optionsWithPolicy, packageManifest);
118
122
  return;
123
+ case "architect":
124
+ await runArchitectCli(optionsWithPolicy);
125
+ return;
126
+ case "plans":
127
+ await runPlansCli(optionsWithPolicy);
128
+ return;
129
+ case "conversation":
130
+ await runConversationCli(optionsWithPolicy);
131
+ return;
132
+ case "ui":
133
+ await runUiCli(optionsWithPolicy);
134
+ return;
135
+ case "start":
136
+ await runCockpitCli(optionsWithPolicy);
137
+ return;
119
138
  case "review":
120
139
  await runGlobalReview(optionsWithPolicy, packageManifest);
121
140
  return;
@@ -133,17 +152,11 @@ export async function runCli(argv) {
133
152
  return;
134
153
  case "mcp": {
135
154
  const { runMcpCli } = await import("./global/mcp-install.js");
136
- await runMcpCli({
137
- mcpAction: optionsWithPolicy.mcpAction ?? "serve",
138
- mcpClient: optionsWithPolicy.mcpClient ?? "cursor",
139
- yes: optionsWithPolicy.yes === true,
140
- json: optionsWithPolicy.json === true,
141
- cwd: optionsWithPolicy.cwd,
142
- cwdExplicit: optionsWithPolicy.cwdExplicit,
155
+ await runMcpCli(buildMcpCliOptions(optionsWithPolicy, {
143
156
  packageRoot,
144
157
  packageName: packageManifest.name,
145
158
  version: packageManifest.version
146
- });
159
+ }));
147
160
  return;
148
161
  }
149
162
  case "connections": {
@@ -457,6 +470,19 @@ function argsWantsWorkspaceScope(args) {
457
470
  return false;
458
471
  }
459
472
 
473
+ export function buildMcpCliOptions(options = {}, extras = {}) {
474
+ return {
475
+ mcpAction: options.mcpAction ?? "serve",
476
+ mcpClient: options.mcpClient ?? "cursor",
477
+ yes: options.yes === true,
478
+ json: options.json === true,
479
+ cwd: options.cwd,
480
+ cwdExplicit: options.cwdExplicit,
481
+ workspaceBound: options.workspaceBound === true,
482
+ ...extras
483
+ };
484
+ }
485
+
460
486
  export function parseArgs(argv) {
461
487
  const args = [...argv];
462
488
  const firstArg = args[0];
@@ -466,6 +492,7 @@ export function parseArgs(argv) {
466
492
  const options = {
467
493
  cwd: process.cwd(),
468
494
  cwdExplicit: false,
495
+ workspaceBound: false,
469
496
  scope: null,
470
497
  mode: "standard",
471
498
  modeExplicit: false,
@@ -513,6 +540,9 @@ export function parseArgs(argv) {
513
540
  intelligencePaths: [],
514
541
  runsAction: null,
515
542
  runId: null,
543
+ plansAction: null,
544
+ conversationAction: null,
545
+ taskId: null,
516
546
  reviewId: null,
517
547
  lineage: null,
518
548
  reviewsAction: null,
@@ -552,7 +582,8 @@ export function parseArgs(argv) {
552
582
  activeOnly: false,
553
583
  timeoutMs: null,
554
584
  includePrivate: false,
555
- cloudConsent: false
585
+ cloudConsent: false,
586
+ port: null
556
587
  };
557
588
 
558
589
  if (command === "components") {
@@ -571,6 +602,12 @@ export function parseArgs(argv) {
571
602
  parseRunsAction(args, options);
572
603
  }
573
604
 
605
+ if (command === "plans") {
606
+ parsePlansAction(args, options);
607
+ }
608
+
609
+ if (command === "conversation") parseConversationAction(args, options);
610
+
574
611
  if (command === "reviews") {
575
612
  parseReviewsAction(args, options);
576
613
  }
@@ -606,6 +643,7 @@ export function parseArgs(argv) {
606
643
  options.cwd = resolve(args[++index]);
607
644
  options.cwdExplicit = true;
608
645
  }
646
+ else if (arg === "--workspace-bound") options.workspaceBound = true;
609
647
  else if (arg === "--scope") options.scope = parseScope(args[++index]);
610
648
  else if (arg.startsWith("--scope=")) options.scope = parseScope(arg.slice("--scope=".length));
611
649
  else if (arg === "--mode") {
@@ -687,7 +725,7 @@ export function parseArgs(argv) {
687
725
  else if (arg === "--simple") options.simple = true;
688
726
  else if (arg === "--task" || arg.startsWith("--task=")) {
689
727
  const taskValue = arg.startsWith("--task=") ? arg.slice("--task=".length) : args[++index];
690
- if (command === "run") options.task = taskValue;
728
+ if (command === "run" || command === "architect" || command === "conversation") options.task = taskValue;
691
729
  else options.intelligenceTask = taskValue;
692
730
  }
693
731
  else if (arg === "--prompt") options.intelligencePrompt = args[++index];
@@ -698,6 +736,8 @@ export function parseArgs(argv) {
698
736
  else if (arg.startsWith("--agent=")) options.agent = arg.slice("--agent=".length);
699
737
  else if (arg === "--model") options.model = args[++index];
700
738
  else if (arg.startsWith("--model=")) options.model = arg.slice("--model=".length);
739
+ else if (arg === "--role") options.role = args[++index];
740
+ else if (arg.startsWith("--role=")) options.role = arg.slice("--role=".length);
701
741
  else if (arg === "--strategy") {
702
742
  options.strategy = normalizeRunStrategy(requireFlagValue("--strategy", args[++index]));
703
743
  } else if (arg.startsWith("--strategy=")) {
@@ -727,6 +767,8 @@ export function parseArgs(argv) {
727
767
  else if (arg.startsWith("--timeout=")) options.timeoutMs = parsePositiveInt(arg.slice("--timeout=".length), "timeout") * 1000;
728
768
  else if (arg === "--include-private") options.includePrivate = true;
729
769
  else if (arg === "--cloud-consent") options.cloudConsent = true;
770
+ else if (arg === "--port") options.port = parsePositiveInt(args[++index], "port");
771
+ else if (arg.startsWith("--port=")) options.port = parsePositiveInt(arg.slice("--port=".length), "port");
730
772
  else if (arg === "--base") options.base = requireFlagValue("--base", args[++index]);
731
773
  else if (arg.startsWith("--base=")) options.base = requireFlagValue("--base", arg.slice("--base=".length));
732
774
  else if (arg === "--commit") options.commit = requireFlagValue("--commit", args[++index]);
@@ -743,7 +785,7 @@ export function parseArgs(argv) {
743
785
  else throw new Error(`Unknown option "${arg}".`);
744
786
  }
745
787
 
746
- if (command === "run" && !options.task && args.length > 0) {
788
+ if ((command === "run" || command === "architect") && !options.task && args.length > 0) {
747
789
  options.task = args.join(" ").trim();
748
790
  }
749
791
 
@@ -760,6 +802,24 @@ export function parseArgs(argv) {
760
802
  return { command, options, isImplicitCommand: implicitCommand };
761
803
  }
762
804
 
805
+ function parseConversationAction(args, options) {
806
+ const action = args[0];
807
+ if (!action || action.startsWith("-")) {
808
+ options.conversationAction = "snapshot";
809
+ return;
810
+ }
811
+ if (!new Set(["snapshot", "architect", "show", "approve", "reject", "execute", "cancel"]).has(action)) {
812
+ throw new Error(`Unknown conversation action "${action}".`);
813
+ }
814
+ args.shift();
815
+ options.conversationAction = action;
816
+ if (["show", "approve", "reject", "execute", "cancel"].includes(action)) {
817
+ const taskId = args.shift();
818
+ if (!taskId || taskId.startsWith("-")) throw new Error(`Missing task id for conversation ${action}.`);
819
+ options.taskId = taskId;
820
+ }
821
+ }
822
+
763
823
  function parseComponentsAction(args, options) {
764
824
  const action = args[0];
765
825
  if (!action || action.startsWith("-")) return;
@@ -878,6 +938,26 @@ function parseRunsAction(args, options) {
878
938
  }
879
939
  }
880
940
 
941
+ function parsePlansAction(args, options) {
942
+ const action = args[0];
943
+ if (!action || action.startsWith("-")) {
944
+ options.plansAction = "list";
945
+ return;
946
+ }
947
+ if (!new Set(["list", "show", "approve", "reject"]).has(action)) {
948
+ throw new Error(`Unknown plans action "${action}". Use list, show, approve, or reject.`);
949
+ }
950
+ args.shift();
951
+ options.plansAction = action;
952
+ if (action !== "list") {
953
+ const taskId = args[0];
954
+ if (!taskId || taskId.startsWith("-")) {
955
+ throw new Error(`Missing task id. Use: ${formatCliCommand(`plans ${action} <taskId>`)}`);
956
+ }
957
+ options.taskId = args.shift();
958
+ }
959
+ }
960
+
881
961
  function parseReviewsAction(args, options) {
882
962
  const action = args[0];
883
963
  if (!action || action.startsWith("-")) {
@@ -1053,6 +1133,11 @@ function normalizeCommand(command) {
1053
1133
  if (command === "orchestrator") return "orchestrator";
1054
1134
  if (command === "run") return "run";
1055
1135
  if (command === "runs") return "runs";
1136
+ if (command === "architect") return "architect";
1137
+ if (command === "plans") return "plans";
1138
+ if (command === "conversation") return "conversation";
1139
+ if (command === "ui") return "ui";
1140
+ if (command === "start") return "start";
1056
1141
  if (command === "review") return "review";
1057
1142
  if (command === "reviews") return "reviews";
1058
1143
  if (command === "monitor") return "monitor";
@@ -28,9 +28,9 @@ export function createAgentCapabilityAdapter({
28
28
  return managedAdapter.detect(context);
29
29
  },
30
30
 
31
- inspect(context, { probeImpl = defaultProbe } = {}) {
31
+ inspect(context, { probeImpl = defaultProbe, isExecutableAvailableImpl = isExecutableAvailable } = {}) {
32
32
  const detected = managedAdapter.detect(context);
33
- const cliAvailable = executable ? isExecutableAvailable(executable) : false;
33
+ const cliAvailable = executable ? isExecutableAvailableImpl(executable) : false;
34
34
 
35
35
  if (!detected && !cliAvailable) {
36
36
  return buildInspection({
@@ -0,0 +1,76 @@
1
+ import { resolve } from "node:path";
2
+ import { createArchitecturePlan } from "./architect-manager.js";
3
+ import { PLAN_STATES } from "./architect-types.js";
4
+ import {
5
+ listTaskRecords, readTaskRecord, resolveProjectRoot, transitionTask
6
+ } from "./architect-store.js";
7
+ import { printJson } from "../json-output.js";
8
+ import { commandHeader } from "../brand/index.js";
9
+
10
+ function publicRecord(projectRoot, record) {
11
+ const status = record.status ?? record;
12
+ const planExists = record.planMarkdown != null
13
+ || [PLAN_STATES.AWAITING_APPROVAL, PLAN_STATES.APPROVED, PLAN_STATES.REJECTED].includes(status.state);
14
+ return {
15
+ ...status,
16
+ ...(record.taskMarkdown ? { taskMarkdown: record.taskMarkdown } : {}),
17
+ ...(record.planMarkdown ? { planMarkdown: record.planMarkdown } : {}),
18
+ planPath: status.artifacts?.plan && planExists
19
+ ? (record.paths?.planPath ?? resolve(projectRoot, status.artifacts.plan))
20
+ : null
21
+ };
22
+ }
23
+
24
+ export async function runArchitectCli(options, deps = {}) {
25
+ const createPlan = deps.createPlan ?? createArchitecturePlan;
26
+ const result = await createPlan({ task: options.task, cwd: options.cwd, model: options.model });
27
+ const data = {
28
+ ...publicRecord(result.status.projectRoot, { status: result.status, paths: result.paths }),
29
+ reused: result.reused === true
30
+ };
31
+ if (options.json) printJson(data);
32
+ else {
33
+ console.log(commandHeader(`architecture plan · ${data.taskId}`));
34
+ console.log(`State: ${data.state}`);
35
+ if (data.planPath) {
36
+ console.log(`Plan: ${data.artifacts.plan}`);
37
+ console.log(`Approve: kairo plans approve ${data.taskId}`);
38
+ } else {
39
+ console.log("Planning is still active; no plan artifact is ready yet.");
40
+ }
41
+ }
42
+ return data;
43
+ }
44
+
45
+ export async function runPlansCli(options, deps = {}) {
46
+ const projectRoot = await (deps.resolveRoot ?? resolveProjectRoot)(options.cwd);
47
+ const action = options.plansAction ?? "list";
48
+ if (action === "list") {
49
+ const plans = await (deps.listRecords ?? listTaskRecords)(projectRoot);
50
+ const data = { projectRoot, plans };
51
+ if (options.json) printJson(data);
52
+ else {
53
+ console.log(commandHeader("architecture plans"));
54
+ for (const plan of plans) console.log(` ${plan.taskId} ${plan.state} ${plan.artifacts?.plan ?? ""}`);
55
+ if (!plans.length) console.log(" (no plans)");
56
+ }
57
+ return data;
58
+ }
59
+ if (action === "show") {
60
+ const record = await (deps.readRecord ?? readTaskRecord)(projectRoot, options.taskId);
61
+ if (!record) throw new Error(`Plan "${options.taskId}" not found.`);
62
+ const data = publicRecord(projectRoot, record);
63
+ if (options.json) printJson(data);
64
+ else {
65
+ console.log(commandHeader(`architecture plan · ${data.taskId}`));
66
+ console.log(record.planMarkdown ?? `No plan artifact (${data.state}): ${data.error?.message ?? "planning did not complete"}`);
67
+ }
68
+ return data;
69
+ }
70
+ const state = action === "approve" ? PLAN_STATES.APPROVED : PLAN_STATES.REJECTED;
71
+ const record = await (deps.transition ?? transitionTask)(projectRoot, options.taskId, state);
72
+ const data = publicRecord(projectRoot, record);
73
+ if (options.json) printJson(data);
74
+ else console.log(`${data.taskId}: ${data.state}`);
75
+ return data;
76
+ }
@@ -0,0 +1,146 @@
1
+ import {
2
+ ReviewExecError,
3
+ assertBoundedProcessOk,
4
+ runBoundedProcess
5
+ } from "../runtime/review/review-exec.js";
6
+ import { buildCodexCliEnv } from "../runtime/review/review-codex.js";
7
+
8
+ export const ARCHITECT_CODEX_ERRORS = Object.freeze({
9
+ INVALID_JSONL: "invalid_jsonl",
10
+ MISSING_PLAN: "missing_plan",
11
+ STREAM_ERROR: "stream_error",
12
+ SUBSCRIPTION_AUTH_REQUIRED: "subscription_auth_required"
13
+ });
14
+
15
+ const AUTH_TIMEOUT_MS = 10_000;
16
+ const AUTH_OUTPUT_LIMIT = 8_192;
17
+
18
+ export function buildArchitectCodexArgs({ cwd, model = null } = {}) {
19
+ if (typeof cwd !== "string" || !cwd) throw new Error("Architect requires cwd.");
20
+ const args = [
21
+ "--ask-for-approval", "never",
22
+ "exec", "--json", "--ephemeral", "--ignore-user-config",
23
+ "--sandbox", "read-only", "-C", cwd,
24
+ "-c", "shell_environment_policy.inherit=none"
25
+ ];
26
+ if (model) args.push("-m", String(model));
27
+ args.push("-");
28
+ return args;
29
+ }
30
+
31
+ export function buildArchitectPrompt(task, { contextPack = null } = {}) {
32
+ return [
33
+ "You are the architecture planner for this repository.",
34
+ "Use the bounded Kairo context pack below as primary evidence.",
35
+ "Do not broadly scan the repository. Inspect additional files only when the context pack identifies a concrete gap required to plan safely.",
36
+ "Honor all AGENTS.md and governance instructions included in the context pack.",
37
+ "Do not modify files, run destructive commands, or implement code.",
38
+ "Return a concise implementation plan in Markdown with: Goal, Verified context, Design, Files, Tests, Risks, and Acceptance criteria.",
39
+ "Challenge unsupported assumptions and identify any blocking unknowns.",
40
+ "",
41
+ "TASK",
42
+ String(task).trim(),
43
+ "",
44
+ "BOUNDED KAIRO CONTEXT PACK",
45
+ contextPack?.systemPrompt ?? "No context pack was available; state this limitation in Verified context."
46
+ ].join("\n");
47
+ }
48
+
49
+ export function buildArchitectCodexEnv(sourceEnv = process.env) {
50
+ const env = buildCodexCliEnv(sourceEnv);
51
+ // M1 is subscription-only. Never let an ambient API key or alternate paid
52
+ // endpoint turn subscription exhaustion into PAYG.
53
+ for (const key of ["OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE"]) delete env[key];
54
+ return env;
55
+ }
56
+
57
+ export async function verifyCodexSubscriptionAuth({
58
+ env = process.env, runProcess = runBoundedProcess
59
+ } = {}) {
60
+ const result = await runProcess({
61
+ command: "codex",
62
+ args: ["login", "status"],
63
+ env: buildArchitectCodexEnv(env),
64
+ stdin: null,
65
+ timeoutMs: AUTH_TIMEOUT_MS,
66
+ stdoutLimit: AUTH_OUTPUT_LIMIT,
67
+ stderrLimit: AUTH_OUTPUT_LIMIT
68
+ });
69
+ assertBoundedProcessOk(result);
70
+ const evidence = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
71
+ if (!/(?:^|\n)Logged in using ChatGPT\r?(?:\n|$)/.test(evidence)) {
72
+ throw new ReviewExecError(
73
+ "Codex must be logged in using ChatGPT subscription auth; API-key and unknown auth modes are refused.",
74
+ { code: ARCHITECT_CODEX_ERRORS.SUBSCRIPTION_AUTH_REQUIRED }
75
+ );
76
+ }
77
+ return { mode: "chatgpt" };
78
+ }
79
+
80
+ export function parseArchitectCodexJsonl(stdout) {
81
+ let plan = null;
82
+ let usage = null;
83
+ let streamError = null;
84
+ const raw = String(stdout ?? "");
85
+ const lines = raw.split(/\r?\n/);
86
+ const complete = raw.endsWith("\n") || raw.endsWith("\r\n") ? lines : lines.slice(0, -1);
87
+ for (const line of complete) {
88
+ const trimmed = line.trim();
89
+ if (!trimmed) continue;
90
+ let event;
91
+ try { event = JSON.parse(trimmed); }
92
+ catch (error) {
93
+ throw new ReviewExecError(`Malformed Codex JSONL: ${error.message}`, {
94
+ code: ARCHITECT_CODEX_ERRORS.INVALID_JSONL
95
+ });
96
+ }
97
+ if (!event || typeof event !== "object") {
98
+ throw new ReviewExecError("Malformed Codex JSONL event.", {
99
+ code: ARCHITECT_CODEX_ERRORS.INVALID_JSONL
100
+ });
101
+ }
102
+ if (event.type === "error") streamError = String(event.message ?? "Codex stream error.");
103
+ else if (event.type === "turn.failed") streamError = String(event.error?.message ?? "Codex turn failed.");
104
+ else if (event.type === "turn.completed" && event.usage && typeof event.usage === "object") {
105
+ const input = Number.isFinite(event.usage.input_tokens) ? event.usage.input_tokens : null;
106
+ const output = Number.isFinite(event.usage.output_tokens) ? event.usage.output_tokens : null;
107
+ usage = {
108
+ inputTokens: input,
109
+ outputTokens: output,
110
+ totalTokens: input != null && output != null ? input + output : null,
111
+ cost: null
112
+ };
113
+ } else if (event.type === "item.completed" && event.item?.type === "agent_message"
114
+ && typeof event.item.text === "string") {
115
+ plan = event.item.text;
116
+ }
117
+ }
118
+ if (streamError) {
119
+ throw new ReviewExecError(streamError, { code: ARCHITECT_CODEX_ERRORS.STREAM_ERROR });
120
+ }
121
+ if (typeof plan !== "string" || !plan.trim()) {
122
+ throw new ReviewExecError("Codex JSONL missing final architecture plan.", {
123
+ code: ARCHITECT_CODEX_ERRORS.MISSING_PLAN
124
+ });
125
+ }
126
+ return { plan: plan.trim(), usage };
127
+ }
128
+
129
+ export async function runArchitectCodex({
130
+ task, cwd, model = null, contextPack = null, env = process.env, spawnImpl, timeoutMs,
131
+ runProcess = runBoundedProcess, verifyAuth = verifyCodexSubscriptionAuth
132
+ } = {}) {
133
+ const codexEnv = buildArchitectCodexEnv(env);
134
+ await verifyAuth({ env: codexEnv });
135
+ const result = await runProcess({
136
+ command: "codex",
137
+ args: buildArchitectCodexArgs({ cwd, model }),
138
+ cwd,
139
+ env: codexEnv,
140
+ stdin: buildArchitectPrompt(task, { contextPack }),
141
+ spawnImpl,
142
+ timeoutMs
143
+ });
144
+ assertBoundedProcessOk(result);
145
+ return parseArchitectCodexJsonl(result.stdout);
146
+ }