@kal-elsam/kairo-runtime 0.16.0 → 0.17.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.
- package/CHANGELOG.md +50 -0
- package/package.json +2 -1
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/ux-smoke-test.sh +3 -3
- package/src/cli.js +96 -11
- package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
- package/src/global/architect/architect-cli.js +76 -0
- package/src/global/architect/architect-codex.js +146 -0
- package/src/global/architect/architect-manager.js +125 -0
- package/src/global/architect/architect-store.js +377 -0
- package/src/global/architect/architect-types.js +47 -0
- package/src/global/cli-help.js +10 -1
- package/src/global/cockpit/app.js +475 -0
- package/src/global/cockpit/card.js +111 -0
- package/src/global/cockpit/cli.js +33 -0
- package/src/global/cockpit/gauge.js +31 -0
- package/src/global/cockpit/project-overlay.js +683 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1263 -0
- package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
- package/src/global/conversation/cli.js +53 -0
- package/src/global/conversation/codex-sandbox.js +230 -0
- package/src/global/conversation/cursor-sandbox.js +215 -0
- package/src/global/conversation/project-analysis.js +204 -0
- package/src/global/conversation/project-profile.js +178 -0
- package/src/global/conversation/project-router.js +149 -0
- package/src/global/conversation/project-strategy-store.js +64 -0
- package/src/global/conversation/project-strategy.js +514 -0
- package/src/global/conversation/sanitized-snapshot.js +169 -0
- package/src/global/conversation/secret-scanner.js +71 -0
- package/src/global/conversation/service.js +1063 -0
- package/src/global/conversation/session-store.js +75 -0
- package/src/global/conversation/transcript-store.js +79 -0
- package/src/global/conversation/ui.js +195 -0
- package/src/global/intelligence/capability-scoring.js +480 -0
- package/src/global/intelligence/execution-router.js +444 -0
- package/src/global/intelligence/kairo-telemetry-source.js +59 -0
- package/src/global/intelligence/kairobench-runner.js +85 -0
- package/src/global/intelligence/kairobench-source.js +34 -0
- package/src/global/intelligence/kairobench-tasks.js +47 -0
- package/src/global/intelligence/model-candidate-catalog.js +456 -0
- package/src/global/intelligence/model-capability-registry-sources.js +145 -0
- package/src/global/intelligence/model-capability-registry.js +125 -0
- package/src/global/intelligence/model-intelligence.js +1646 -0
- package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
- package/src/global/intelligence/quick-ask.js +149 -0
- package/src/global/intelligence/role-profiles.js +251 -0
- package/src/global/intelligence/skill-catalog.js +67 -0
- package/src/global/intelligence/subscription-pressure-source.js +41 -0
- package/src/global/mcp/kairo-mcp.js +51 -18
- package/src/global/mcp/work-snapshot-rule.js +4 -2
- package/src/global/mcp/workspace-binding.js +88 -0
- package/src/global/mcp/workspace-mcp-entry.js +74 -0
- package/src/global/mcp-install.js +8 -1
- package/src/global/observability/artificial-analysis-models.js +118 -0
- package/src/global/observability/claude-models.js +31 -0
- package/src/global/observability/claude-usage.js +112 -0
- package/src/global/observability/codex-models.js +96 -0
- package/src/global/observability/codex-usage.js +160 -0
- package/src/global/observability/cursor-auth.js +88 -0
- package/src/global/observability/cursor-models.js +101 -0
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/opencode-models.js +101 -0
- package/src/global/observability/opencode-usage.js +162 -0
- package/src/global/paths.js +49 -2
- package/src/global/profile.js +23 -1
- package/src/global/runtime/execution-adapters/claude.js +63 -30
- package/src/global/runtime/execution-adapters/codex.js +9 -2
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
- package/src/global/runtime/execution-adapters/opencode.js +83 -18
- package/src/global/runtime/execution-worktree-manager.js +924 -0
- package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
- package/src/global/runtime/execution-worktree-store.js +83 -0
- package/src/global/runtime/execution-worktree-types.js +45 -0
- package/src/global/runtime/run-events.js +38 -0
- package/src/global/runtime/run-manager.js +22 -6
- package/src/global/runtime/run-supervisor.js +41 -12
- package/src/global/runtime/usage-manager.js +96 -0
- package/src/global/runtime/usage-store.js +69 -0
- package/src/global/runtime/usage-types.js +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,56 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.17.0 — 2026-09-17 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. Execution-worktree isolation (real git worktrees, per-role
|
|
11
|
+
commit transactions, preview + `git merge --ff-only` apply, cancellation
|
|
12
|
+
and crash recovery), a per-provider budget/usage state machine, automatic
|
|
13
|
+
Builder→Debugger→Tester orchestration, and several real `/project`
|
|
14
|
+
cockpit UX fixes.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Execution-worktree isolation (`execution-worktree-manager.js`): real
|
|
19
|
+
`git worktree add`-backed isolation per task, a per-role commit
|
|
20
|
+
transaction (`beginRoleRun`/`completeRoleRun`) that only ever commits
|
|
21
|
+
a role's own validated diff (Kairo owns every real commit, never the
|
|
22
|
+
agent), `previewWorktreeMerge`/`applyWorktreeMerge` (confirmation
|
|
23
|
+
bound to an exact `{baseSha, finalHeadSha, fingerprint}`, applied only
|
|
24
|
+
via `git merge --ff-only`), and `cancelWorktree`/`reconcileWorktrees`/
|
|
25
|
+
`discardWorktree` for cancellation and crash recovery.
|
|
26
|
+
- `controlledHeadSha`: every state-changing boundary (not just ACTIVE)
|
|
27
|
+
verifies the worktree's real HEAD against the last commit Kairo itself
|
|
28
|
+
produced, closing a rogue-commit bypass reachable while a worktree sat
|
|
29
|
+
idle in PENDING.
|
|
30
|
+
- Per-provider budget/usage state machine (`usage-manager.js`):
|
|
31
|
+
cumulative token/cost tracking per provider, a
|
|
32
|
+
HEALTHY/MODERATE/CONSERVE/CRITICAL/EXHAUSTED classification against a
|
|
33
|
+
configurable `profile.providerTokenBudgets`, and one real enforcement
|
|
34
|
+
point — a new run is refused once its provider is EXHAUSTED.
|
|
35
|
+
- `runOrchestratedChain` (`execution-worktree-orchestrator.js`):
|
|
36
|
+
automatic Builder→Debugger→Tester chaining inside one execution
|
|
37
|
+
worktree, stopping immediately on any real failure. Injects matching
|
|
38
|
+
real project skills (`docs/skills`, `.claude/skills`, etc. — the same
|
|
39
|
+
catalog ASK-mode routing already reads) into each role's task by
|
|
40
|
+
name/path, never their body content.
|
|
41
|
+
- `/project` cockpit overlay: the PROJECT TEAM panel now shows the real
|
|
42
|
+
decision reason behind each role's model, a real bordered panel, the
|
|
43
|
+
cockpit's own live spinner during every in-flight action, and real
|
|
44
|
+
mouse support (clicks now reach the analyst/role picker lists).
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Dashboard and `/project status` showed `qualityTeam` (comparative
|
|
49
|
+
reference) under a "PROJECT TEAM" title while the interactive overlay
|
|
50
|
+
showed the real operational `projectTeam` — both now read the same
|
|
51
|
+
real operational team.
|
|
52
|
+
- MCP snapshot writes require `--workspace-bound` plus an absolute `--cwd`
|
|
53
|
+
that matches `process.cwd()` or the unique `WORKSPACE_FOLDER_PATHS` folder.
|
|
54
|
+
Bound servers expose only `kairo_publish_work_snapshot`; the Cursor
|
|
55
|
+
extension ships that runtime as `dist/kairo-workspace.cjs` and spawns
|
|
56
|
+
absolute Node ≥20 — never PATH `kairo`. See [MCP](docs/mcp.md).
|
|
57
|
+
|
|
8
58
|
## 0.16.0 — 2026-08-13 (Kairo Runtime)
|
|
9
59
|
|
|
10
60
|
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.
|
|
3
|
+
"version": "0.17.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.
|
|
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);
|
package/scripts/ux-smoke-test.sh
CHANGED
|
@@ -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" "
|
|
80
|
-
assert_contains "$CAPTURE_DIR/help.txt" "
|
|
81
|
-
assert_contains "$CAPTURE_DIR/help.txt" "
|
|
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 ?
|
|
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
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PLAN_STATES, ARCHITECT_SCHEMA, createArchitectureRequestKey,
|
|
3
|
+
createTaskId, normalizeArchitectTask, sha256
|
|
4
|
+
} from "./architect-types.js";
|
|
5
|
+
import { runArchitectCodex } from "./architect-codex.js";
|
|
6
|
+
import {
|
|
7
|
+
acquireRequestLock, findReusableRequest, prepareTaskDirectory, resolveHead,
|
|
8
|
+
resolveProjectRoot, writeAtomicText, writeTaskArtifacts
|
|
9
|
+
} from "./architect-store.js";
|
|
10
|
+
import { writeAtomicJson } from "../runtime/write-atomic-json.js";
|
|
11
|
+
import { compileContextPack } from "../intelligence/context-compiler.js";
|
|
12
|
+
|
|
13
|
+
const REQUEST_DISCOVERY_ATTEMPTS = 40;
|
|
14
|
+
const REQUEST_DISCOVERY_DELAY_MS = 25;
|
|
15
|
+
|
|
16
|
+
async function waitForReusableRequest(projectRoot, requestKey) {
|
|
17
|
+
for (let attempt = 0; attempt < REQUEST_DISCOVERY_ATTEMPTS; attempt += 1) {
|
|
18
|
+
const active = await findReusableRequest(projectRoot, requestKey, {
|
|
19
|
+
states: [PLAN_STATES.DRAFT, PLAN_STATES.AWAITING_APPROVAL]
|
|
20
|
+
});
|
|
21
|
+
if (active) return active;
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, REQUEST_DISCOVERY_DELAY_MS));
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function prepareUniqueTask(projectRoot, task, now) {
|
|
28
|
+
const baseId = createTaskId(task, now);
|
|
29
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
30
|
+
const suffix = attempt === 0 ? "" : `-${attempt + 1}`;
|
|
31
|
+
const taskId = `${baseId.slice(0, 96 - suffix.length)}${suffix}`;
|
|
32
|
+
try {
|
|
33
|
+
const paths = await prepareTaskDirectory(projectRoot, taskId, { createExclusive: true });
|
|
34
|
+
return { taskId, paths };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.code !== "EEXIST") throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Unable to allocate a unique architecture task id.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createArchitecturePlan({
|
|
43
|
+
task, cwd = process.cwd(), model = null, now = new Date(), runCodex = runArchitectCodex,
|
|
44
|
+
resolveRoot = resolveProjectRoot, resolveGitHead = resolveHead,
|
|
45
|
+
compileContext = compileContextPack, acquireLock = acquireRequestLock
|
|
46
|
+
} = {}) {
|
|
47
|
+
if (typeof task !== "string" || !task.trim()) throw new Error("Architect task is required.");
|
|
48
|
+
const projectRoot = await resolveRoot(cwd);
|
|
49
|
+
const baseHead = resolveGitHead(projectRoot);
|
|
50
|
+
const normalizedTask = normalizeArchitectTask(task);
|
|
51
|
+
const normalizedModel = model == null ? null : String(model).trim() || null;
|
|
52
|
+
const requestKey = createArchitectureRequestKey({
|
|
53
|
+
projectRoot, baseHead, task: normalizedTask, model: normalizedModel
|
|
54
|
+
});
|
|
55
|
+
const reusable = await findReusableRequest(projectRoot, requestKey);
|
|
56
|
+
if (reusable) return { status: reusable.status, paths: reusable.paths, reused: true };
|
|
57
|
+
|
|
58
|
+
const lock = await acquireLock(projectRoot, requestKey, { now });
|
|
59
|
+
if (!lock.acquired) {
|
|
60
|
+
const active = await waitForReusableRequest(projectRoot, requestKey);
|
|
61
|
+
if (active) return { status: active.status, paths: active.paths, reused: true };
|
|
62
|
+
throw new Error("An identical architecture request is already starting. Retry shortly.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let paths;
|
|
66
|
+
try {
|
|
67
|
+
const afterLock = await findReusableRequest(projectRoot, requestKey);
|
|
68
|
+
if (afterLock) return { status: afterLock.status, paths: afterLock.paths, reused: true };
|
|
69
|
+
const orphan = await findReusableRequest(projectRoot, requestKey, { states: [PLAN_STATES.DRAFT] });
|
|
70
|
+
if (orphan) {
|
|
71
|
+
await writeAtomicJson(orphan.paths.statusPath, {
|
|
72
|
+
...orphan.status,
|
|
73
|
+
state: PLAN_STATES.FAILED,
|
|
74
|
+
updatedAt: now.toISOString(),
|
|
75
|
+
error: {
|
|
76
|
+
code: "stale_architect_request",
|
|
77
|
+
message: "Recovered an interrupted architecture request whose owner is no longer active."
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const prepared = await prepareUniqueTask(projectRoot, normalizedTask, now);
|
|
82
|
+
const taskId = prepared.taskId;
|
|
83
|
+
paths = prepared.paths;
|
|
84
|
+
const taskMarkdown = `# Task\n\n${normalizedTask}\n`;
|
|
85
|
+
const createdAt = now.toISOString();
|
|
86
|
+
const draft = {
|
|
87
|
+
schema: ARCHITECT_SCHEMA, taskId, state: PLAN_STATES.DRAFT,
|
|
88
|
+
requestKey, provider: "codex", model: normalizedModel, projectRoot, baseHead,
|
|
89
|
+
taskDigest: sha256(normalizedTask), taskArtifactDigest: sha256(taskMarkdown),
|
|
90
|
+
planArtifactDigest: null, usage: null,
|
|
91
|
+
createdAt, updatedAt: createdAt, decisionAt: null, decisionHead: null,
|
|
92
|
+
artifacts: { task: ".ai/tasks/" + taskId + "/task.md", plan: ".ai/tasks/" + taskId + "/plan.md" }
|
|
93
|
+
};
|
|
94
|
+
await writeAtomicText(paths.taskPath, taskMarkdown);
|
|
95
|
+
await writeAtomicJson(paths.statusPath, draft);
|
|
96
|
+
try {
|
|
97
|
+
const contextPack = await compileContext({
|
|
98
|
+
workspaceRoot: projectRoot, task: normalizedTask, relevantPaths: [], includePrivate: false
|
|
99
|
+
});
|
|
100
|
+
const result = await runCodex({
|
|
101
|
+
task: normalizedTask, cwd: projectRoot, model: normalizedModel, contextPack
|
|
102
|
+
});
|
|
103
|
+
const planMarkdown = `# Architecture Plan\n\n${result.plan.trim()}\n`;
|
|
104
|
+
const awaiting = {
|
|
105
|
+
...draft,
|
|
106
|
+
state: PLAN_STATES.AWAITING_APPROVAL,
|
|
107
|
+
planArtifactDigest: sha256(planMarkdown),
|
|
108
|
+
usage: result.usage ?? null,
|
|
109
|
+
updatedAt: new Date().toISOString()
|
|
110
|
+
};
|
|
111
|
+
await writeTaskArtifacts({
|
|
112
|
+
projectRoot, taskId, task: normalizedTask, plan: result.plan, status: awaiting
|
|
113
|
+
});
|
|
114
|
+
return { status: awaiting, paths, reused: false };
|
|
115
|
+
} catch (error) {
|
|
116
|
+
await writeAtomicJson(paths.statusPath, {
|
|
117
|
+
...draft, state: PLAN_STATES.FAILED, updatedAt: new Date().toISOString(),
|
|
118
|
+
error: { code: error?.code ?? "architect_failed", message: error?.message ?? String(error) }
|
|
119
|
+
});
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
await lock.release();
|
|
124
|
+
}
|
|
125
|
+
}
|