aperta-cli 1.0.0-beta.2 → 1.0.0-beta.3

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.
@@ -4,7 +4,6 @@ import { access, copyFile, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile
4
4
  import { createConnection } from "node:net";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, isAbsolute, join, normalize, sep } from "node:path";
7
- import { createInterface } from "node:readline";
8
7
  import { promisify } from "node:util";
9
8
  import { requestProviderAction } from "./coach.js";
10
9
  import { createRepositorySnapshot, diffSnapshots } from "./git.js";
@@ -13,6 +12,7 @@ import { cleanExecutionOutput, safeEnvironment } from "./execution.js";
13
12
  import { assertSkillAllowsAction, selectAgentSkill, skillPrompt } from "./skills.js";
14
13
  import { privateCachePath } from "./storage.js";
15
14
  import { initializeStore } from "./ledger.js";
15
+ import { agentVRuntime, eventAction } from "./agent-runtime.js";
16
16
  const execFileAsync = promisify(execFile);
17
17
  const runs = new Map();
18
18
  // A first implementation often consumes most of the original 24-step budget.
@@ -188,7 +188,20 @@ function finalizeEvidence(run) {
188
188
  }
189
189
  export function classifyAgentError(reason) {
190
190
  const error = reason instanceof Error ? reason : new Error(String(reason));
191
+ const failureCode = reason?.code;
191
192
  const message = error.message.toLowerCase();
193
+ if (failureCode === "cancelled")
194
+ return "UserAborted";
195
+ if (failureCode === "timeout")
196
+ return "Timeout";
197
+ if (["authentication-required", "invocation-failed", "empty-response"].includes(failureCode ?? ""))
198
+ return "ProviderError";
199
+ if (failureCode === "engine-unavailable")
200
+ return "UnexpectedEnvironment";
201
+ if (["invalid-json", "output-invalid"].includes(failureCode ?? ""))
202
+ return "InvalidModelOutput";
203
+ if (["permission-denied", "unsupported-capability", "configuration-invalid"].includes(failureCode ?? ""))
204
+ return "InvalidArguments";
192
205
  if (error.name === "AbortError" || /\bcancel(?:ed|led)?\b|user aborted/.test(message))
193
206
  return "UserAborted";
194
207
  if (/timed? out|timeout/.test(message))
@@ -983,144 +996,46 @@ export async function runModelAgent(root, intent, config, signal, fetcher = fetc
983
996
  await cleanupWorkspace(root, workspace, worktree);
984
997
  }
985
998
  }
986
- function externalWorkspacePath(value, workspace) {
987
- if (typeof value !== "string" || !value.trim())
988
- return undefined;
989
- const path = value.replaceAll("\\", "/");
990
- const root = workspace.replaceAll("\\", "/").replace(/\/$/, "");
991
- if (path === root)
992
- return ".";
993
- if (path.startsWith(`${root}/`))
994
- return path.slice(root.length + 1);
995
- return path.match(/\/aperta-agent-[^/]+\/(.+)$/)?.[1] ?? path;
996
- }
997
- function externalToolDetail(tool, path, input) {
998
- const target = path ? ` ${path}` : "";
999
- if (/^(?:read|view|open)$/.test(tool))
1000
- return `Inspecting${target || " a repository file"}.`;
1001
- if (/^(?:edit|write|patch|apply_patch)$/.test(tool))
1002
- return `Updating${target || " repository content"}.`;
1003
- if (/^(?:glob|grep|search|find)$/.test(tool)) {
1004
- const query = [input.pattern, input.query, input.glob].find((value) => typeof value === "string");
1005
- return query ? `Searching the repository for ${String(query).slice(0, 180)}.` : "Searching the repository.";
1006
- }
1007
- if (/^(?:run|bash|shell|command)$/.test(tool))
1008
- return "Running a bounded repository command.";
1009
- return `${tool.replaceAll("_", " ")} completed.`;
1010
- }
1011
- /** Converts provider-specific JSONL into the small, human-readable activity vocabulary Aperta owns. */
1012
- export function normalizeExternalRuntimeEvent(event, workspace) {
1013
- const type = typeof event.type === "string" ? event.type.toLowerCase() : "event";
1014
- const subtype = typeof event.subtype === "string" ? event.subtype.toLowerCase() : "";
1015
- if (type === "result" || type === "system" || subtype === "init")
1016
- return null;
1017
- const message = event.message && typeof event.message === "object" ? event.message : undefined;
1018
- const content = Array.isArray(message?.content) ? message.content : Array.isArray(event.content) ? event.content : [];
1019
- const claudeTool = content.find((item) => item && typeof item === "object" && item.type === "tool_use");
1020
- const source = claudeTool ?? event;
1021
- const input = source.input && typeof source.input === "object" ? source.input
1022
- : event.args && typeof event.args === "object" ? event.args
1023
- : event.arguments && typeof event.arguments === "object" ? event.arguments
1024
- : {};
1025
- const serialized = JSON.stringify(event);
1026
- const name = [source.name, event.tool_name, event.toolName]
1027
- .find((value) => typeof value === "string");
1028
- const fallbackTool = serialized.match(/"([A-Za-z]+)ToolCall"/)?.[1];
1029
- const tool = (name ?? fallbackTool ?? (type.includes("tool") ? "tool" : "")).toLowerCase();
1030
- if (!tool || tool === "assistant")
1031
- return null;
1032
- const rawPath = [input.file_path, input.path, input.filePath, event.file_path, event.path, event.filePath]
1033
- .find((value) => typeof value === "string");
1034
- const path = externalWorkspacePath(rawPath, workspace);
1035
- const failed = event.is_error === true || [event.status, subtype].some((value) => value === "failed" || value === "error");
1036
- return { action: tool, detail: externalToolDetail(tool, path, input), path, status: failed ? "error" : "success" };
1037
- }
1038
- function cursorResultText(event) {
1039
- const candidates = [];
1040
- const visit = (value, key = "") => {
1041
- if (typeof value === "string" && /^(?:result|text|content|message|summary)$/.test(key) && value.trim())
1042
- candidates.push(value.trim());
1043
- else if (Array.isArray(value))
1044
- value.forEach((item) => visit(item, key));
1045
- else if (value && typeof value === "object")
1046
- for (const [childKey, child] of Object.entries(value))
1047
- visit(child, childKey);
1048
- };
1049
- visit(event);
1050
- return candidates.sort((a, b) => b.length - a.length)[0]?.slice(0, 2_000) ?? "";
1051
- }
1052
- export function externalRuntimeArgs(runtime, workspace, prompt, skill) {
1053
- const claudeTools = skill && !skill.allowedTools.includes("repository.write") ? "Read,Glob,Grep" : "Read,Edit,Write,Glob,Grep";
1054
- const args = runtime.kind === "cursor"
1055
- ? ["-p", prompt, "--force", "--output-format", "stream-json"]
1056
- : runtime.kind === "claude"
1057
- ? ["-p", prompt, "--output-format", "stream-json", "--verbose", "--max-turns", "48", "--permission-mode", "acceptEdits", "--tools", claudeTools, "--disable-slash-commands", "--no-session-persistence"]
1058
- : ["run", "--format", "json", "--pure", "--auto", "--dir", workspace, prompt];
1059
- if (runtime.model)
1060
- args.push(runtime.kind === "opencode" ? "--model" : "--model", runtime.model);
1061
- return args;
1062
- }
1063
- async function executeExternalTurn(root, workspace, run, runtime, prompt, signal) {
1064
- const args = externalRuntimeArgs(runtime, workspace, prompt, run.skill);
999
+ async function executeExternalTurn(root, workspace, run, runtime, prompt, signal, runtimeEngine = agentVRuntime) {
1065
1000
  const started = Date.now();
1066
- const externalEnvironment = safeEnvironment();
1067
- if (process.env.CURSOR_API_KEY)
1068
- externalEnvironment.CURSOR_API_KEY = process.env.CURSOR_API_KEY;
1069
- if (runtime.kind === "claude" && process.env.ANTHROPIC_API_KEY)
1070
- externalEnvironment.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
1071
- if (runtime.kind === "claude" && process.env.CLAUDE_CODE_OAUTH_TOKEN)
1072
- externalEnvironment.CLAUDE_CODE_OAUTH_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN;
1073
- if (runtime.kind === "opencode")
1074
- externalEnvironment.OPENCODE_CONFIG_CONTENT = JSON.stringify({ permission: { "*": "deny", read: "allow", edit: run.skill.allowedTools.includes("repository.write") ? "allow" : "deny", glob: "allow", grep: "allow", list: "allow", lsp: "allow", bash: "deny", webfetch: "deny", task: "deny", skill: "deny", external_directory: "deny" } });
1075
- const child = spawn(runtime.command, args, { cwd: workspace, stdio: ["ignore", "pipe", "pipe"], env: externalEnvironment });
1076
- let stderr = "", summary = "", timedOut = false;
1077
- child.stderr?.on("data", (chunk) => { stderr = `${stderr}${String(chunk)}`.slice(-24_000); });
1078
- const abort = () => child.kill("SIGTERM");
1079
- signal?.addEventListener("abort", abort, { once: true });
1080
- const timeout = setTimeout(() => { timedOut = true; child.kill("SIGTERM"); }, 15 * 60_000);
1081
- const exit = new Promise((resolve, reject) => {
1082
- child.once("error", reject);
1083
- child.once("close", (code) => resolve(code));
1084
- });
1085
1001
  try {
1086
- if (!child.stdout)
1087
- throw new Error("Cursor CLI did not expose an output stream");
1088
- const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
1089
- for await (const line of lines) {
1090
- if (!line.trim())
1091
- continue;
1092
- let event;
1093
- try {
1094
- event = JSON.parse(line);
1095
- }
1096
- catch {
1097
- continue;
1098
- }
1099
- const result = cursorResultText(event);
1100
- if (result)
1101
- summary = result;
1102
- const record = normalizeExternalRuntimeEvent(event, workspace);
1103
- if (record && run.actions.length < 400) {
1104
- run.actions.push({ index: run.actions.length + 1, ...record, ts: new Date().toISOString() });
1105
- run.telemetry.toolCalls++;
1106
- await persist(root, run);
1107
- }
1108
- }
1109
- const code = await exit;
1002
+ if (runtime.kind === "aperta")
1003
+ throw new Error("Aperta Native does not use the local CLI adapter");
1004
+ const result = await runtimeEngine.run({
1005
+ kind: runtime.kind,
1006
+ model: runtime.model || undefined,
1007
+ workspace,
1008
+ workspaceAccess: run.skill.allowedTools.includes("repository.write") ? "workspace-write" : "read-only",
1009
+ projectId: run.repo,
1010
+ runId: run.id,
1011
+ prompt,
1012
+ abortSignal: signal,
1013
+ events: {
1014
+ async emit(event) {
1015
+ if (event.type === "run.started")
1016
+ run.provenance = event.provenance;
1017
+ const action = eventAction(event);
1018
+ if (action && run.actions.length < 400) {
1019
+ run.actions.push({ index: run.actions.length + 1, ...action, ts: new Date().toISOString() });
1020
+ await persist(root, run);
1021
+ }
1022
+ },
1023
+ },
1024
+ });
1025
+ run.provenance = result.provenance;
1110
1026
  run.telemetry.providerCalls++;
1111
- run.telemetry.providerLatencyMs += Date.now() - started;
1112
- if (signal?.aborted)
1113
- throw new DOMException("Canceled", "AbortError");
1114
- const label = runtime.kind === "cursor" ? "Cursor" : runtime.kind === "claude" ? "Claude Code" : "OpenCode";
1115
- if (timedOut)
1116
- throw new Error(`${label} exceeded Aperta's 15-minute turn limit`);
1117
- if (code !== 0)
1118
- throw new Error(`${label} exited with code ${code}: ${cleanExecutionOutput(stderr).slice(-4_000) || "no diagnostic output"}`);
1119
- return summary;
1027
+ run.telemetry.providerLatencyMs += result.durationMs;
1028
+ run.telemetry.toolCalls += result.activityCount;
1029
+ run.actions.push({ index: run.actions.length + 1, action: "runtime", detail: `${result.activityCount} transport event${result.activityCount === 1 ? "" : "s"} · ${result.attempts} attempt${result.attempts === 1 ? "" : "s"}.`, status: "success", durationMs: result.durationMs, ts: new Date().toISOString() });
1030
+ await persist(root, run);
1031
+ return result.summary;
1120
1032
  }
1121
- finally {
1122
- clearTimeout(timeout);
1123
- signal?.removeEventListener("abort", abort);
1033
+ catch (error) {
1034
+ if (!run.telemetry.providerCalls) {
1035
+ run.telemetry.providerCalls++;
1036
+ run.telemetry.providerLatencyMs += Date.now() - started;
1037
+ }
1038
+ throw error;
1124
1039
  }
1125
1040
  }
1126
1041
  export async function runExternalAgent(root, intent, runtime, signal, context = {}) {
@@ -1130,7 +1045,7 @@ export async function runExternalAgent(root, intent, runtime, signal, context =
1130
1045
  if (cleanIntent.length < 10 || cleanIntent.length > 4_000)
1131
1046
  throw new Error("Describe the change in 10 to 4,000 characters");
1132
1047
  const previousRuns = context.previousRuns ?? [];
1133
- const runtimeLabel = runtime.kind === "cursor" ? "Cursor" : runtime.kind === "claude" ? "Claude Code" : "OpenCode";
1048
+ const runtimeLabel = runtime.kind === "codex" ? "Codex CLI" : runtime.kind === "cursor" ? "Cursor" : runtime.kind === "claude" ? "Claude Code" : "OpenCode";
1134
1049
  const selectedSkill = selectAgentSkill(cleanIntent);
1135
1050
  const run = { id: randomUUID(), conversationId: conversationId(context.conversationId), turnIndex: previousRuns.length + 1, repo: root.split("/").at(-1) ?? "repository", intent: cleanIntent, status: "running", provider: runtime.kind, model: runtime.model || `${runtimeLabel} default`, createdAt: new Date().toISOString(), files: [], patch: "", actions: [], capabilities: [], skill: selectedSkill, verification: { status: "unavailable", plan: [], attempts: [] }, contract: defaultExecutionContract(cleanIntent, [], selectedSkill), promotion: { status: "review-required", allowed: false, requiresHumanReview: true, reason: "The run has not produced reviewable evidence yet." }, telemetry: { providerCalls: 0, providerLatencyMs: 0, toolCalls: 0, toolLatencyMs: 0, errors: [] }, context: { maxInputChars: MAX_AGENT_INPUT_CHARS, estimatedMaxInputTokens: Math.ceil(MAX_AGENT_INPUT_CHARS / 4), lastInputChars: 0, estimatedLastInputTokens: 0, maxOutputTokens: AGENT_OUTPUT_TOKENS, retryMaxOutputTokens: AGENT_RETRY_OUTPUT_TOKENS } };
1136
1051
  await persist(root, run);
@@ -1154,7 +1069,7 @@ export async function runExternalAgent(root, intent, runtime, signal, context =
1154
1069
  prompt += "\n\nAperta could not detect a supported project verification command in this repository. Explain that harness-level limitation clearly; do not claim that your runtime's lack of Bash is the reason.";
1155
1070
  run.context.lastInputChars = prompt.length;
1156
1071
  run.context.estimatedLastInputTokens = Math.ceil(prompt.length / 4);
1157
- let summary = await executeExternalTurn(root, workspace, run, runtime, prompt, signal) || `${runtimeLabel} completed the requested turn.`;
1072
+ let summary = await executeExternalTurn(root, workspace, run, runtime, prompt, signal, context.runtimeEngine) || `${runtimeLabel} completed the requested turn.`;
1158
1073
  const initialCandidate = await createRepositorySnapshot(workspace), initialDiff = await diffSnapshots(workspace, turnStart, initialCandidate);
1159
1074
  if (initialDiff.files.length && !run.skill.allowedTools.includes("repository.write"))
1160
1075
  throw new Error(`${run.skill.label} is read-only, but ${runtimeLabel} attempted to modify ${initialDiff.files.length} repository file${initialDiff.files.length === 1 ? "" : "s"}. Aperta discarded the isolated changes.`);
@@ -1180,7 +1095,7 @@ export async function runExternalAgent(root, intent, runtime, signal, context =
1180
1095
  run.context.lastInputChars = prompt.length;
1181
1096
  run.context.estimatedLastInputTokens = Math.ceil(prompt.length / 4);
1182
1097
  await persist(root, run);
1183
- summary = await executeExternalTurn(root, workspace, run, runtime, prompt, signal) || summary;
1098
+ summary = await executeExternalTurn(root, workspace, run, runtime, prompt, signal, context.runtimeEngine) || summary;
1184
1099
  }
1185
1100
  const after = await createRepositorySnapshot(workspace), diff = await diffSnapshots(workspace, before, after);
1186
1101
  run.resultTree = after.tree;