@kal-elsam/kairo-runtime 0.15.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +76 -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 +106 -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 +12 -1
  13. package/src/global/cockpit/app.js +475 -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 +683 -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 +1263 -0
  21. package/src/global/control-plane/attention.js +141 -0
  22. package/src/global/control-plane/build-report.js +146 -0
  23. package/src/global/control-plane/cli.js +36 -0
  24. package/src/global/control-plane/constants.js +38 -0
  25. package/src/global/control-plane/gentle-adapters.js +183 -0
  26. package/src/global/control-plane/provider.js +69 -0
  27. package/src/global/control-plane/review-status.js +115 -0
  28. package/src/global/control-plane/sdd-status.js +49 -0
  29. package/src/global/control-plane/team.js +63 -0
  30. package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
  31. package/src/global/conversation/cli.js +53 -0
  32. package/src/global/conversation/codex-sandbox.js +230 -0
  33. package/src/global/conversation/cursor-sandbox.js +215 -0
  34. package/src/global/conversation/project-analysis.js +204 -0
  35. package/src/global/conversation/project-profile.js +178 -0
  36. package/src/global/conversation/project-router.js +149 -0
  37. package/src/global/conversation/project-strategy-store.js +64 -0
  38. package/src/global/conversation/project-strategy.js +514 -0
  39. package/src/global/conversation/sanitized-snapshot.js +169 -0
  40. package/src/global/conversation/secret-scanner.js +71 -0
  41. package/src/global/conversation/service.js +1063 -0
  42. package/src/global/conversation/session-store.js +75 -0
  43. package/src/global/conversation/transcript-store.js +79 -0
  44. package/src/global/conversation/ui.js +195 -0
  45. package/src/global/intelligence/capability-scoring.js +480 -0
  46. package/src/global/intelligence/execution-router.js +444 -0
  47. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  48. package/src/global/intelligence/kairobench-runner.js +85 -0
  49. package/src/global/intelligence/kairobench-source.js +34 -0
  50. package/src/global/intelligence/kairobench-tasks.js +47 -0
  51. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  52. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  53. package/src/global/intelligence/model-capability-registry.js +125 -0
  54. package/src/global/intelligence/model-intelligence.js +1646 -0
  55. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  56. package/src/global/intelligence/quick-ask.js +149 -0
  57. package/src/global/intelligence/role-profiles.js +251 -0
  58. package/src/global/intelligence/skill-catalog.js +67 -0
  59. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  60. package/src/global/mcp/kairo-mcp.js +51 -18
  61. package/src/global/mcp/work-snapshot-rule.js +4 -2
  62. package/src/global/mcp/workspace-binding.js +88 -0
  63. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  64. package/src/global/mcp-install.js +8 -1
  65. package/src/global/observability/artificial-analysis-models.js +118 -0
  66. package/src/global/observability/claude-models.js +31 -0
  67. package/src/global/observability/claude-usage.js +112 -0
  68. package/src/global/observability/codex-models.js +96 -0
  69. package/src/global/observability/codex-usage.js +160 -0
  70. package/src/global/observability/cursor-auth.js +88 -0
  71. package/src/global/observability/cursor-models.js +101 -0
  72. package/src/global/observability/gentle-probe.js +30 -2
  73. package/src/global/observability/huggingface-leaderboard.js +97 -0
  74. package/src/global/observability/index.js +2 -1
  75. package/src/global/observability/opencode-models.js +101 -0
  76. package/src/global/observability/opencode-usage.js +162 -0
  77. package/src/global/paths.js +49 -2
  78. package/src/global/profile.js +23 -1
  79. package/src/global/runtime/execution-adapters/claude.js +63 -30
  80. package/src/global/runtime/execution-adapters/codex.js +9 -2
  81. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  82. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  83. package/src/global/runtime/execution-worktree-manager.js +924 -0
  84. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  85. package/src/global/runtime/execution-worktree-store.js +83 -0
  86. package/src/global/runtime/execution-worktree-types.js +45 -0
  87. package/src/global/runtime/run-events.js +38 -0
  88. package/src/global/runtime/run-manager.js +22 -6
  89. package/src/global/runtime/run-supervisor.js +41 -12
  90. package/src/global/runtime/usage-manager.js +96 -0
  91. package/src/global/runtime/usage-store.js +69 -0
  92. package/src/global/runtime/usage-types.js +62 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Official Gentle review status v2/v3 only. Never read authority inventory.
3
+ */
4
+ import { isAbsolute } from "node:path";
5
+ import { SUPPORTED_CONTRACT } from "../observability/gentle-probe.js";
6
+
7
+ export const REVIEW_STATUS_SCHEMAS = Object.freeze([
8
+ "gentle-ai.review-integration.status/v2",
9
+ "gentle-ai.review-integration.status/v3"
10
+ ]);
11
+
12
+ const INVENTORY_SCHEMA = "gentle-ai.review-authority-status/v1";
13
+ const UNSAFE_TOKEN = /[|;&$`\n\r]/;
14
+ const PLACEHOLDER = /^<[^>]+>$/;
15
+
16
+ export const GENTLE_224_BOOTSTRAP =
17
+ "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --next-transition";
18
+ export const GENTLE_230_BOOTSTRAP =
19
+ "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --agent claude-code --next-transition";
20
+
21
+ const failArgv = () => ({ ok: false, error: "gentle_incompatible", binary: null, argv: null });
22
+
23
+ export function bootstrapCommandFromProbe(probe) {
24
+ return probe?.evidence?.find((row) => row?.kind === "bootstrap" && row.command)?.command ?? null;
25
+ }
26
+
27
+ export function argvFromBootstrap(command, { repo, binaryPath }) {
28
+ if (typeof command !== "string" || !command.trim() || UNSAFE_TOKEN.test(command)) return failArgv();
29
+ if (typeof binaryPath !== "string" || !isAbsolute(binaryPath)) return failArgv();
30
+ if (typeof repo !== "string" || !repo) return failArgv();
31
+ const tokens = command.trim().split(/\s+/);
32
+ if (tokens[0] !== "gentle-ai") return failArgv();
33
+ const args = [];
34
+ const rest = tokens.slice(1);
35
+ for (let i = 0; i < rest.length; i += 1) {
36
+ const tok = rest[i];
37
+ if (UNSAFE_TOKEN.test(tok)) return failArgv();
38
+ if (tok === "--cwd") {
39
+ const next = rest[i + 1];
40
+ const needsRepo = next === "<repo>" || next == null || next.startsWith("-");
41
+ args.push("--cwd", needsRepo ? repo : next);
42
+ if (!needsRepo || next === "<repo>") i += 1;
43
+ continue;
44
+ }
45
+ if (PLACEHOLDER.test(tok)) return failArgv();
46
+ args.push(tok);
47
+ }
48
+ if (args[0] !== "review" || args[1] !== "status") return failArgv();
49
+ return { ok: true, error: null, binary: binaryPath, argv: args };
50
+ }
51
+
52
+ function isObject(value) {
53
+ return value != null && typeof value === "object" && !Array.isArray(value);
54
+ }
55
+
56
+ function publishedReceipt(receiptField) {
57
+ if (typeof receiptField === "string" && receiptField) return receiptField;
58
+ if (!isObject(receiptField)) return null;
59
+ if (typeof receiptField.id === "string" && receiptField.id) return receiptField.id;
60
+ if (typeof receiptField.digest === "string" && receiptField.digest) return receiptField.digest;
61
+ return null;
62
+ }
63
+
64
+ function publishedGate(payload) {
65
+ if (typeof payload.gate === "string" && payload.gate) return payload.gate;
66
+ if (isObject(payload.receipt) && typeof payload.receipt.gate === "string" && payload.receipt.gate) {
67
+ return payload.receipt.gate;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Fail closed unless schema/contract are official. Pass next_transition through unaltered.
74
+ */
75
+ export function mapOfficialReviewStatus(payload) {
76
+ if (!isObject(payload)) {
77
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
78
+ }
79
+ if (
80
+ payload.schema === INVENTORY_SCHEMA
81
+ || payload.authoritative === true
82
+ || Array.isArray(payload.entries)
83
+ ) {
84
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
85
+ }
86
+ if (!REVIEW_STATUS_SCHEMAS.includes(payload.schema)) {
87
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
88
+ }
89
+ if (payload.contract != null && payload.contract !== SUPPORTED_CONTRACT) {
90
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
91
+ }
92
+
93
+ const nextTransition = Object.prototype.hasOwnProperty.call(payload, "next_transition")
94
+ ? payload.next_transition
95
+ : null;
96
+ const receipt = publishedReceipt(payload.receipt);
97
+ const gate = publishedGate(payload);
98
+ const applicability = typeof payload.applicability === "string" ? payload.applicability : null;
99
+ const action = typeof payload.action === "string" ? payload.action : null;
100
+ const receiptStatus = isObject(payload.receipt) && typeof payload.receipt.status === "string"
101
+ ? payload.receipt.status
102
+ : null;
103
+
104
+ const review = (receipt || gate || applicability || action)
105
+ ? {
106
+ lineageId: null,
107
+ state: applicability ?? action,
108
+ status: receiptStatus,
109
+ receipt,
110
+ gate
111
+ }
112
+ : null;
113
+
114
+ return { ok: true, error: null, review, nextTransition };
115
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Official `sdd-status --json` projection. No inferred phase/route/next.
3
+ */
4
+ import { NO_ACTIVE_WORKFLOW, WORKFLOW_KIND } from "./constants.js";
5
+
6
+ export const SDD_STATUS_SCHEMA_PREFIX = "gentle-ai.sdd-status";
7
+ export const SDD_STATUS_ARGS = Object.freeze(["sdd-status", "--json"]);
8
+
9
+ function isObject(value) {
10
+ return value != null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ export function mapOfficialSddStatus(payload) {
14
+ if (!isObject(payload)) {
15
+ return { ok: false, error: "gentle_incompatible", projection: null };
16
+ }
17
+ const schemaName = typeof payload.schemaName === "string"
18
+ ? payload.schemaName
19
+ : (typeof payload.schema === "string" ? payload.schema : null);
20
+ if (!schemaName || !schemaName.startsWith(SDD_STATUS_SCHEMA_PREFIX)) {
21
+ return { ok: false, error: "gentle_incompatible", projection: null };
22
+ }
23
+ const changeName = typeof payload.changeName === "string" && payload.changeName
24
+ ? payload.changeName
25
+ : null;
26
+ const nextRecommended = typeof payload.nextRecommended === "string" && payload.nextRecommended
27
+ ? payload.nextRecommended
28
+ : null;
29
+ return {
30
+ ok: true,
31
+ error: null,
32
+ projection: { schemaName, changeName, nextRecommended }
33
+ };
34
+ }
35
+
36
+ export function applySddProjection(workflow, projection) {
37
+ workflow.sdd = projection;
38
+ workflow.changeName = projection.changeName;
39
+ workflow.phase = null;
40
+ if (projection.changeName) {
41
+ workflow.kind = WORKFLOW_KIND.SDD;
42
+ workflow.active = true;
43
+ workflow.label = "SDD";
44
+ } else if (workflow.kind !== WORKFLOW_KIND.REVIEW) {
45
+ workflow.kind = WORKFLOW_KIND.NONE;
46
+ workflow.active = false;
47
+ workflow.label = NO_ACTIVE_WORKFLOW;
48
+ }
49
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Normalize declared fleets into honest control-plane team nodes.
3
+ * Live requires OpenCode active-agent evidence — never idle sessions.
4
+ */
5
+ import { HONESTY } from "./constants.js";
6
+
7
+ function honestyForNode(node, { platform, hasLiveActivity }) {
8
+ if (node?.opaque === true) return HONESTY.OPAQUE;
9
+ if (platform === "opencode" && hasLiveActivity) return HONESTY.LIVE;
10
+ return HONESTY.DECLARED;
11
+ }
12
+
13
+ export function normalizeTeam(connectionsReport = {}) {
14
+ const fleets = Array.isArray(connectionsReport.fleets) ? connectionsReport.fleets : [];
15
+ const activity = connectionsReport.activity ?? null;
16
+ const hasLiveActivity = Boolean(
17
+ activity
18
+ && (
19
+ (Array.isArray(activity.agents) && activity.agents.some((a) => a?.state === "active"))
20
+ || (typeof activity.activeCount === "number" && activity.activeCount > 0)
21
+ || activity.active === true
22
+ )
23
+ );
24
+
25
+ // Cursor configured agents are declared topology (not live); only opaque nodes stay opaque.
26
+ const platforms = fleets.map((fleet) => {
27
+ const platform = fleet?.platform ?? "unknown";
28
+ const orch = fleet?.orchestrator ?? null;
29
+ return {
30
+ platform,
31
+ honesty: honestyForNode(fleet, { platform, hasLiveActivity }),
32
+ source: fleet?.source ?? null,
33
+ orchestrator: orch
34
+ ? {
35
+ id: orch.id ?? null,
36
+ model: orch.modelShort ?? orch.model ?? null,
37
+ honesty: honestyForNode(orch, { platform, hasLiveActivity }),
38
+ role: orch.mode ?? "orchestrator"
39
+ }
40
+ : null,
41
+ agents: (Array.isArray(fleet?.minions) ? fleet.minions : []).map((m) => ({
42
+ id: m.id,
43
+ model: m.modelShort ?? m.model ?? null,
44
+ role: m.role ?? null,
45
+ honesty: m?.opaque === true && platform === "cursor"
46
+ ? HONESTY.DECLARED
47
+ : honestyForNode(m, { platform, hasLiveActivity: false })
48
+ }))
49
+ };
50
+ });
51
+
52
+ return {
53
+ platforms,
54
+ // Only surface activity when there is live evidence (active workers).
55
+ activity: hasLiveActivity ? activity : null,
56
+ fleetNote: connectionsReport.fleetNote
57
+ ?? "Declared config topology — not live token usage.",
58
+ orchestratorAuthority: connectionsReport.orchestratorAuthority ?? null,
59
+ connections: Array.isArray(connectionsReport.connections)
60
+ ? connectionsReport.connections
61
+ : []
62
+ };
63
+ }
@@ -0,0 +1,251 @@
1
+ // A neutral contract for Bootstrap Analysis, so conversation/service.js's
2
+ // runBootstrapAnalysis doesn't special-case providers inline — each real
3
+ // provider gets its own adapter, and adding a new one (Cursor, a future
4
+ // verified Claude boundary, etc.) means adding a factory here, not
5
+ // branching inside runBootstrapAnalysis.
6
+ //
7
+ // Every adapter exposes:
8
+ // adapterId, modelId
9
+ // checkEligibility(): Promise<{ eligible, reason?, isolation, canaryTested }>
10
+ // isolation and canaryTested are separate axes — WHO enforces the
11
+ // boundary is not the same question as WHETHER it's been proven:
12
+ // isolation: "verified" | "restricted" | "unverified" — WHO enforces
13
+ // it. "verified" = an OS/kernel-enforced boundary (Codex's
14
+ // sandbox-exec, see codex-sandbox.js) that holds even if the CLI's
15
+ // own logic has a bug. "restricted" = an application-enforced
16
+ // boundary — the provider's own CLI/tool-permission logic (Claude's
17
+ // --restricted). "unverified" = no real boundary available.
18
+ // canaryTested: boolean — WHETHER that boundary has actually been
19
+ // empirically proven (a real canary read outside it was attempted
20
+ // and denied), as opposed to merely documented/assumed from
21
+ // --help text or a vendor's own claim. Both Codex's and Claude's
22
+ // adapters are canaryTested: true today; a provider could in
23
+ // principle be "restricted" but NOT canaryTested if its isolation
24
+ // claim were never independently checked — never conflate the two.
25
+ // analyze({ question, snapshotRoot, timeoutMs }): Promise<{status, answer, error}>
26
+ // same response shape intelligence/quick-ask.js's askProvider already
27
+ // returns, so callers don't need to branch on adapter type downstream.
28
+ //
29
+ // This module only wires the providers that already have a real
30
+ // implementation (Codex, Claude). Requesting an adapterId with no real
31
+ // implementation yet returns an honest "not implemented" ineligible
32
+ // adapter — never a silent fallback to a provider the caller didn't ask
33
+ // for.
34
+
35
+ import { askProvider as defaultAskProvider } from "../intelligence/quick-ask.js";
36
+ import {
37
+ getCodexIsolationStatus as defaultGetCodexIsolationStatus,
38
+ runCodexSandboxedBootstrap as defaultRunCodexSandboxedBootstrap
39
+ } from "./codex-sandbox.js";
40
+ import { verifyClaudeSubscriptionAuth as defaultVerifyClaudeSubscriptionAuth } from "../runtime/execution-adapters/claude.js";
41
+ import { readClaudeModels as defaultReadClaudeModels } from "../observability/claude-models.js";
42
+ import { readCursorModels as defaultReadCursorModels } from "../observability/cursor-models.js";
43
+ import { probeCursorAuth as defaultProbeCursorAuth } from "../observability/cursor-auth.js";
44
+ import {
45
+ getCursorIsolationStatus as defaultGetCursorIsolationStatus,
46
+ runCursorSandboxedBootstrap as defaultRunCursorSandboxedBootstrap
47
+ } from "./cursor-sandbox.js";
48
+
49
+ export function createCodexBootstrapAnalyzerAdapter({ modelId, deps = {} } = {}) {
50
+ const runSandboxed = deps.runCodexSandboxedBootstrap ?? defaultRunCodexSandboxedBootstrap;
51
+ const getIsolation = deps.getCodexIsolationStatus ?? defaultGetCodexIsolationStatus;
52
+ return {
53
+ adapterId: "codex",
54
+ modelId,
55
+ async checkEligibility() {
56
+ const isolation = await getIsolation(deps.isolationDeps ?? {});
57
+ return {
58
+ eligible: isolation.available,
59
+ reason: isolation.available ? undefined : isolation.reason,
60
+ isolation: isolation.available ? "verified" : "unverified",
61
+ // The sandbox-exec mechanism itself was empirically canary-tested
62
+ // (a real absolute-path read outside the confined root was denied,
63
+ // see codex-sandbox.js's own header) — `available` reflects that
64
+ // the same proven mechanism is usable here (platform + binary
65
+ // present), not a fresh proof on every call.
66
+ canaryTested: isolation.available
67
+ };
68
+ },
69
+ async analyze({ question, snapshotRoot, timeoutMs }) {
70
+ return runSandboxed({ question, model: modelId, snapshotRoot, timeoutMs, deps: deps.isolationDeps ?? {} });
71
+ }
72
+ };
73
+ }
74
+
75
+ export function createClaudeBootstrapAnalyzerAdapter({ modelId, deps = {} } = {}) {
76
+ const ask = deps.askProvider ?? defaultAskProvider;
77
+ const verifyAuth = deps.verifyClaudeSubscriptionAuth ?? defaultVerifyClaudeSubscriptionAuth;
78
+ const listModels = deps.readClaudeModels ?? defaultReadClaudeModels;
79
+ return {
80
+ adapterId: "claude",
81
+ modelId,
82
+ // Real, integral eligibility — not a hardcoded claim:
83
+ // 1. CLI + authentication: reuses execution-adapters/claude.js's own
84
+ // `claude auth status` check (the same real one gating a Claude
85
+ // execution run) — an unauthenticated or missing CLI fails here,
86
+ // never silently reported eligible.
87
+ // 2. Model availability: checked against Claude's documented model
88
+ // catalog (observability/claude-models.js). Claude's CLI has no
89
+ // live model-discovery command (verified via its own --help, see
90
+ // that module's own header) — this catches an unknown/typo'd
91
+ // modelId, though it can't prove live per-account entitlement the
92
+ // way Codex/OpenCode's live catalogs can.
93
+ // 3. isolation: "restricted" (application-enforced, by the claude
94
+ // CLI's own in-process tool-permission logic — not an OS kernel
95
+ // sandbox like Codex's sandbox-exec, so a bug in that logic could
96
+ // theoretically be bypassed, unlike a kernel boundary).
97
+ // canaryTested: true — --restricted's actual confinement was
98
+ // empirically canary-tested (not assumed): a real absolute-path
99
+ // read outside cwd came back in the JSON output's own
100
+ // `permission_denials` array (Claude's Read tool itself refused
101
+ // it), while an in-bounds read succeeded with an empty
102
+ // `permission_denials`. That proof is WHETHER it was tested, not
103
+ // WHO enforces it — it does not make this "verified"; only a
104
+ // kernel-enforced boundary earns that label.
105
+ async checkEligibility() {
106
+ try {
107
+ await verifyAuth({});
108
+ } catch (error) {
109
+ return { eligible: false, reason: error?.message ?? String(error), isolation: "unverified", canaryTested: false };
110
+ }
111
+ if (modelId) {
112
+ const catalog = listModels();
113
+ const known = catalog.models.some((m) => m.id === modelId);
114
+ if (!known) {
115
+ return { eligible: false, reason: `"${modelId}" is not in Claude's documented model catalog.`, isolation: "unverified", canaryTested: false };
116
+ }
117
+ }
118
+ return { eligible: true, isolation: "restricted", canaryTested: true };
119
+ },
120
+ async analyze({ question, snapshotRoot, timeoutMs }) {
121
+ return ask({ provider: "claude", question, model: modelId, cwd: snapshotRoot, timeoutMs });
122
+ }
123
+ };
124
+ }
125
+
126
+ const CURSOR_AUTO_IDS = new Set(["auto", "cursor:auto", "cursor-auto"]);
127
+ const CURSOR_AUTO_CANONICAL = "cursor:auto";
128
+ const CURSOR_ANALYZE_TIMEOUT_MS = 180_000;
129
+
130
+ export function createCursorBootstrapAnalyzerAdapter({ modelId, deps = {} } = {}) {
131
+ const listModels = deps.readCursorModels ?? defaultReadCursorModels;
132
+ const probeAuth = deps.probeCursorAuth ?? defaultProbeCursorAuth;
133
+ const getIsolation = deps.getCursorIsolationStatus ?? defaultGetCursorIsolationStatus;
134
+ const runSandboxed = deps.runCursorSandboxedBootstrap ?? defaultRunCursorSandboxedBootstrap;
135
+ const isAuto = modelId != null && CURSOR_AUTO_IDS.has(String(modelId).toLowerCase());
136
+ // Cursor Auto is a distinct, real candidate identity, never an implicit
137
+ // default for a missing selection — its own outcomes are ALWAYS
138
+ // attributed to this canonical "cursor:auto" id, never to a guessed
139
+ // inner model (Cursor never discloses which model actually answered in
140
+ // Auto mode). A caller must explicitly choose it.
141
+ const normalizedModelId = isAuto ? CURSOR_AUTO_CANONICAL : modelId;
142
+ return {
143
+ adapterId: "cursor",
144
+ modelId: normalizedModelId,
145
+ // Real eligibility — currently, honestly, negative. Real gates, none
146
+ // skipped or assumed:
147
+ // 1. A modelId must actually be provided — either a real explicit
148
+ // model or the canonical "cursor:auto" opaque-router candidate;
149
+ // an absent selection is never silently defaulted to either.
150
+ // 2. For an EXPLICIT model: checked against the real per-account
151
+ // catalog (observability/cursor-models.js's readCursorModels — a
152
+ // real `cursor-agent models` call, exit-status-checked). Cursor
153
+ // Auto is exempt from this specific check (it's a routing mode,
154
+ // not a listed catalog model) — but exempting the catalog check
155
+ // must never also exempt authentication: `cursor-agent status`/
156
+ // `whoami` do NOT reliably reflect whether a real invocation will
157
+ // work (verified empirically — status can report "Logged in"
158
+ // while a real -p call still fails with "Authentication
159
+ // required"), so Auto is separately gated on
160
+ // observability/cursor-auth.js's probeCursorAuth, a real
161
+ // invocation-based probe, not the unreliable status/whoami claim.
162
+ // 3. Isolation proof, for BOTH explicit models and Cursor Auto alike:
163
+ // Cursor's OWN --sandbox enabled does NOT confine reads (verified
164
+ // empirically — a real out-of-bounds absolute-path read under
165
+ // --sandbox enabled alone succeeded and disclosed real content).
166
+ // The real boundary is cursor-sandbox.js's external macOS
167
+ // sandbox-exec wrapper (mirroring codex-sandbox.js), independently
168
+ // canary-tested and proven to hold: an in-bounds read succeeds, an
169
+ // out-of-bounds one is denied ("Permission denied"). isolation:
170
+ // "verified" + canaryTested: true only when that mechanism is
171
+ // actually available (macOS + sandbox-exec present) — never on
172
+ // any other platform, and never via Cursor's own --sandbox flag.
173
+ async checkEligibility() {
174
+ if (!modelId) {
175
+ return {
176
+ eligible: false,
177
+ reason: "No Cursor model selection was provided — pass an explicit model or \"cursor:auto\".",
178
+ isolation: "unverified", canaryTested: false
179
+ };
180
+ }
181
+ if (!isAuto) {
182
+ const catalog = await listModels();
183
+ if (catalog.status !== "measured") {
184
+ return {
185
+ eligible: false, reason: `Could not read Cursor's real model catalog: ${catalog.error ?? catalog.status}`,
186
+ isolation: "unverified", canaryTested: false
187
+ };
188
+ }
189
+ if (catalog.models.length === 0) {
190
+ return { eligible: false, reason: "No models are enabled for this Cursor account.", isolation: "unverified", canaryTested: false };
191
+ }
192
+ if (!catalog.models.some((m) => m.id === modelId)) {
193
+ return {
194
+ eligible: false, reason: `"${modelId}" is not in this account's real Cursor model catalog.`,
195
+ isolation: "unverified", canaryTested: false
196
+ };
197
+ }
198
+ } else {
199
+ const auth = await probeAuth();
200
+ if (auth.status !== "measured") {
201
+ return {
202
+ eligible: false, reason: `Could not determine whether Cursor Auto can actually be invoked: ${auth.reason ?? auth.status}`,
203
+ isolation: "unverified", canaryTested: false
204
+ };
205
+ }
206
+ if (!auth.authenticated) {
207
+ return { eligible: false, reason: auth.reason, isolation: "unverified", canaryTested: false };
208
+ }
209
+ }
210
+ const isolation = await getIsolation(deps.isolationDeps ?? {});
211
+ if (!isolation.available) {
212
+ return { eligible: false, reason: isolation.reason, isolation: "unverified", canaryTested: false };
213
+ }
214
+ return { eligible: true, isolation: "verified", canaryTested: true };
215
+ },
216
+ // Routed through cursor-sandbox.js's external sandbox-exec wrapper —
217
+ // never a bare `cursor-agent` spawn, since Cursor's own --sandbox
218
+ // enabled does not confine reads (see that module's header for the
219
+ // full empirical finding). Cursor Auto omits a model id; the wrapper
220
+ // itself omits --model entirely for that case.
221
+ async analyze({ question, snapshotRoot, timeoutMs = CURSOR_ANALYZE_TIMEOUT_MS }) {
222
+ return runSandboxed({
223
+ question, model: isAuto ? null : modelId, snapshotRoot, timeoutMs, deps: deps.isolationDeps ?? {}
224
+ });
225
+ }
226
+ };
227
+ }
228
+
229
+ const ADAPTER_FACTORIES = Object.freeze({
230
+ codex: createCodexBootstrapAnalyzerAdapter,
231
+ claude: createClaudeBootstrapAnalyzerAdapter,
232
+ cursor: createCursorBootstrapAnalyzerAdapter
233
+ });
234
+
235
+ /**
236
+ * @param {string} adapterId
237
+ * @param {{modelId: string, deps?: object}} [options]
238
+ * @returns {{adapterId: string, modelId: string, checkEligibility: Function, analyze: Function}}
239
+ */
240
+ export function createBootstrapAnalyzerAdapter(adapterId, { modelId, deps = {} } = {}) {
241
+ const factory = ADAPTER_FACTORIES[adapterId];
242
+ if (!factory) {
243
+ const reason = `No Bootstrap Analyzer adapter implemented for "${adapterId}" yet.`;
244
+ return {
245
+ adapterId, modelId,
246
+ checkEligibility() { return { eligible: false, reason, isolation: "unverified", canaryTested: false }; },
247
+ async analyze() { throw new Error(reason); }
248
+ };
249
+ }
250
+ return factory({ modelId, deps });
251
+ }
@@ -0,0 +1,53 @@
1
+ import { createConversationService } from "./service.js";
2
+ import { printJson } from "../json-output.js";
3
+
4
+ /**
5
+ * The scripted CLI's own preview/confirm contract, mirroring the cockpit
6
+ * and browser local UI exactly: no `--confirm` is always a read-only
7
+ * preview (service.planExecution — never reserves quota or starts a run);
8
+ * `--confirm` re-fetches that same preview fresh, right before executing,
9
+ * and executes exactly what it shows. PROJECT TEAM is the sole authority
10
+ * for execution — `--role` is required (never inferred from task text),
11
+ * and there is no `--model`/`--agent` override left to bypass it; only a
12
+ * confirmed `confirmationTarget` from the fresh preview ever reaches
13
+ * executePlan.
14
+ * @param {ReturnType<typeof createConversationService>} service
15
+ * @param {object} options
16
+ */
17
+ async function runExecuteAction(service, options) {
18
+ if (!options.role) {
19
+ throw new Error(`Missing --role. PROJECT TEAM is the sole authority for execution — pick the real role this task is for (see /project or 'conversation snapshot' for the active team's roles).`);
20
+ }
21
+ const preview = await service.planExecution({ cwd: options.cwd, taskId: options.taskId, role: options.role });
22
+ if (!options.confirm) return preview;
23
+
24
+ if (!preview.confirmationTarget) {
25
+ throw new Error(`Cannot execute "${options.taskId}": ${preview.why}`);
26
+ }
27
+ return service.executePlan({ cwd: options.cwd, taskId: options.taskId, confirmationTarget: preview.confirmationTarget });
28
+ }
29
+
30
+ export async function runConversationCli(options, deps = {}) {
31
+ const service = deps.service ?? createConversationService(deps);
32
+ const action = options.conversationAction ?? "snapshot";
33
+ let result;
34
+ if (action === "snapshot") result = await service.snapshot({ cwd: options.cwd });
35
+ else if (action === "architect") {
36
+ result = await service.submitArchitecture({ cwd: options.cwd, task: options.task, model: options.model });
37
+ } else if (action === "show") {
38
+ result = await service.showPlan({ cwd: options.cwd, taskId: options.taskId });
39
+ } else if (action === "approve" || action === "reject") {
40
+ result = await service.decidePlan({
41
+ cwd: options.cwd,
42
+ taskId: options.taskId,
43
+ decision: action === "approve" ? "approved" : "rejected"
44
+ });
45
+ } else if (action === "execute") {
46
+ result = await runExecuteAction(service, options);
47
+ } else if (action === "cancel") {
48
+ result = await service.cancelExecution({ cwd: options.cwd, taskId: options.taskId });
49
+ } else throw new Error(`Unknown conversation action "${action}".`);
50
+ if (options.json) printJson(result);
51
+ else console.log(JSON.stringify(result, null, 2));
52
+ return result;
53
+ }