@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.
- package/CHANGELOG.md +76 -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 +106 -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 +12 -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/control-plane/attention.js +141 -0
- package/src/global/control-plane/build-report.js +146 -0
- package/src/global/control-plane/cli.js +36 -0
- package/src/global/control-plane/constants.js +38 -0
- package/src/global/control-plane/gentle-adapters.js +183 -0
- package/src/global/control-plane/provider.js +69 -0
- package/src/global/control-plane/review-status.js +115 -0
- package/src/global/control-plane/sdd-status.js +49 -0
- package/src/global/control-plane/team.js +63 -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/gentle-probe.js +30 -2
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/index.js +2 -1
- 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
|
@@ -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
|
+
}
|