@tryarcanist/cli 0.1.165 → 0.1.167
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/README.md +34 -1
- package/dist/index.js +244 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,12 +159,45 @@ Repeatable `--uploaded-file <path>` flags attach local UTF-8 text files to the p
|
|
|
159
159
|
|
|
160
160
|
`--cold` is a deprecated no-op retained for backward compatibility. Sessions always start from a fresh sandbox (the warm sandbox pool was removed), so the flag has no effect.
|
|
161
161
|
|
|
162
|
-
`--idempotency-key <uuid>` is for manually retrying a create request that may have reached the server.
|
|
162
|
+
`--idempotency-key <uuid>` is for manually retrying a create request that may have reached the server.
|
|
163
|
+
The CLI derives separate session and prompt idempotency keys from the provided value.
|
|
163
164
|
|
|
164
165
|
`--onboarding` creates an onboarding session: the agent authors the repo's `.arcanist.json`, `.arcanist/` runtime files, `ARCANIST.md`, and conditionally `.arcanist/sandbox.yaml` + `.arcanist/sandbox.layer.Dockerfile` (when the base sandbox lacks a needed toolchain), proves what it can inside its sandbox, and opens the setup PR ready for review. The onboarding behavior is driven by the bridge's canonical onboarding playbook, not the prompt, so `--onboarding` needs no prompt: `arcanist sessions create <repo> --onboarding --wait`. Any prompt supplied alongside `--onboarding` is ignored. Team use; not part of the external API surface.
|
|
165
166
|
|
|
166
167
|
JSON mode returns `{sessionId, sessionUrl?, repoUrl, model?, agentRuntimeBackend?, reasoningEffort?, baseBranch?, startBranch?, continuePrUrl?, continueMode?, onboarding?, promptId?}`. `sessionUrl` is emitted only when the server returns it; `agentRuntimeBackend` only when `--backend` is passed. With `--wait`, JSON mode also includes best-effort result fields when available: `prUrl?`, `publishedBranch?`, and `lastBranch?`.
|
|
167
168
|
|
|
169
|
+
### `arcanist sessions qa <pr-url>`
|
|
170
|
+
|
|
171
|
+
Starts a QA verification session for an existing GitHub pull request.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123
|
|
175
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --model gpt-5.4
|
|
176
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --backend claude_code --model claude-opus-4-8
|
|
177
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --reasoning-effort xhigh
|
|
178
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --wait
|
|
179
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --idempotency-key 1f0e6f1a-...
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The PR URL must be `https://github.com/<owner>/<repo>/pull/<number>`.
|
|
183
|
+
The CLI derives the repo from that URL, creates a QA session with `qa: true` and `targetPrUrl`, then enqueues the verifier prompt.
|
|
184
|
+
Inspect progress with the session URL or the session events stream.
|
|
185
|
+
|
|
186
|
+
`--backend` picks the agent runtime backend: `codex` (default), `claude_code`, or `opencode`.
|
|
187
|
+
`--model` must be valid for the chosen backend, and the CLI rejects mismatches before any network call.
|
|
188
|
+
When `--model` is omitted, the backend default is used.
|
|
189
|
+
|
|
190
|
+
`--wait` blocks until the enqueued QA prompt finishes and exits non-zero if the prompt fails.
|
|
191
|
+
JSON mode waits quietly and prints the create payload after the prompt completes successfully.
|
|
192
|
+
`--poll-interval <ms>` tunes completion polling.
|
|
193
|
+
|
|
194
|
+
`--idempotency-key <uuid>` is for manually retrying a QA request that may have reached the server.
|
|
195
|
+
The CLI derives separate session and prompt idempotency keys from the provided value.
|
|
196
|
+
If session creation succeeds but prompt enqueue fails, retry the same command with the same `--idempotency-key`.
|
|
197
|
+
|
|
198
|
+
JSON mode returns `{sessionId, sessionUrl?, repoUrl, targetPrUrl, model?, agentRuntimeBackend?, reasoningEffort?, promptId?}`.
|
|
199
|
+
Active-verifier and per-PR run-limit conflicts both use exit code `4`; active-verifier errors include the existing session handle when the server returns it.
|
|
200
|
+
|
|
168
201
|
### `arcanist sessions send <session-id> [prompt]`
|
|
169
202
|
|
|
170
203
|
Sends a follow-up message to an existing session.
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,11 @@ import { Command } from "commander";
|
|
|
7
7
|
// src/api.ts
|
|
8
8
|
import { createRequire } from "module";
|
|
9
9
|
|
|
10
|
+
// ../../shared/utils/errors.ts
|
|
11
|
+
function stringifyError(error) {
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
// ../../shared/utils/url.ts
|
|
11
16
|
function normalizeBaseUrl(url) {
|
|
12
17
|
return url.replace(/\/+$/, "");
|
|
@@ -122,7 +127,7 @@ async function apiRequest(config, path, init) {
|
|
|
122
127
|
headers
|
|
123
128
|
});
|
|
124
129
|
} catch (err) {
|
|
125
|
-
throw new CliError("server", `Network error: ${
|
|
130
|
+
throw new CliError("server", `Network error: ${stringifyError(err)}`);
|
|
126
131
|
}
|
|
127
132
|
if (!res.ok) {
|
|
128
133
|
const body = await res.text().catch(() => "");
|
|
@@ -504,6 +509,12 @@ var AGENT_RUNTIME_BACKENDS = [
|
|
|
504
509
|
CLAUDE_CODE_AGENT_RUNTIME_BACKEND,
|
|
505
510
|
OPENCODE_AGENT_RUNTIME_BACKEND
|
|
506
511
|
];
|
|
512
|
+
var AGENT_RUNTIME_BACKEND_NAMES = {
|
|
513
|
+
[CODEX_AGENT_RUNTIME_BACKEND]: "Codex",
|
|
514
|
+
[CLAUDE_CODE_AGENT_RUNTIME_BACKEND]: "Claude Code",
|
|
515
|
+
// "opencode" is intentionally lowercase to match the project's brand name.
|
|
516
|
+
[OPENCODE_AGENT_RUNTIME_BACKEND]: "opencode"
|
|
517
|
+
};
|
|
507
518
|
function isAgentRuntimeBackend(value) {
|
|
508
519
|
return AGENT_RUNTIME_BACKENDS.includes(value);
|
|
509
520
|
}
|
|
@@ -536,7 +547,9 @@ var AnthropicModel = {
|
|
|
536
547
|
Sonnet46: "claude-sonnet-4-6"
|
|
537
548
|
};
|
|
538
549
|
var BasetenModel = {
|
|
539
|
-
Glm47: "glm-4.7"
|
|
550
|
+
Glm47: "glm-4.7",
|
|
551
|
+
GptOss120B: "gpt-oss-120b",
|
|
552
|
+
KimiK27Code: "kimi-k2.7-code"
|
|
540
553
|
};
|
|
541
554
|
var MODEL_PROVIDERS_SET = /* @__PURE__ */ new Set([
|
|
542
555
|
"openai",
|
|
@@ -707,6 +720,33 @@ var MODEL_REGISTRY = [
|
|
|
707
720
|
providerModelId: "zai-org/GLM-4.7",
|
|
708
721
|
pricing: { inputPerMillion: 0.6, outputPerMillion: 2.2, cacheReadPerMillion: 0.12 },
|
|
709
722
|
sessionStart: { eligible: true, isDefault: true }
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
id: BasetenModel.GptOss120B,
|
|
726
|
+
name: "GPT OSS 120B",
|
|
727
|
+
provider: "baseten",
|
|
728
|
+
backends: [OPENCODE_AGENT_RUNTIME_BACKEND],
|
|
729
|
+
contextWindow: 128e3,
|
|
730
|
+
// Verified 2026-07-04 against Baseten Model APIs docs and model library:
|
|
731
|
+
// wire id `openai/gpt-oss-120b`, 128k served context/output, OpenAI SDK
|
|
732
|
+
// compatible, Apache 2.0, tool calling supported for all Model APIs.
|
|
733
|
+
providerModelId: "openai/gpt-oss-120b",
|
|
734
|
+
pricing: { inputPerMillion: 0.1, outputPerMillion: 0.5 },
|
|
735
|
+
sessionStart: { eligible: true }
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
id: BasetenModel.KimiK27Code,
|
|
739
|
+
name: "Kimi K2.7 Code",
|
|
740
|
+
provider: "baseten",
|
|
741
|
+
backends: [OPENCODE_AGENT_RUNTIME_BACKEND],
|
|
742
|
+
contextWindow: 262e3,
|
|
743
|
+
// Verified 2026-07-04 against Baseten Model APIs docs, model library, and
|
|
744
|
+
// changelog: wire id `moonshotai/Kimi-K2.7-Code`, 262k served
|
|
745
|
+
// context/output, OpenAI SDK compatible, MIT license, tool calling
|
|
746
|
+
// supported for all Model APIs.
|
|
747
|
+
providerModelId: "moonshotai/Kimi-K2.7-Code",
|
|
748
|
+
pricing: { inputPerMillion: 0.95, outputPerMillion: 4, cacheReadPerMillion: 0.16 },
|
|
749
|
+
sessionStart: { eligible: true }
|
|
710
750
|
}
|
|
711
751
|
];
|
|
712
752
|
var MODEL_PROVIDER_NAMES = {
|
|
@@ -1006,7 +1046,7 @@ async function automationApiFetchText(config, path, init) {
|
|
|
1006
1046
|
}
|
|
1007
1047
|
function mapAutomationApiError(err) {
|
|
1008
1048
|
if (!(err instanceof ApiError)) {
|
|
1009
|
-
return err instanceof CliError ? err : new CliError("server",
|
|
1049
|
+
return err instanceof CliError ? err : new CliError("server", stringifyError(err));
|
|
1010
1050
|
}
|
|
1011
1051
|
const parsed = parseApiErrorBody2(err.body);
|
|
1012
1052
|
const serverCode = parsed?.error;
|
|
@@ -1053,6 +1093,19 @@ function sleep(ms) {
|
|
|
1053
1093
|
}
|
|
1054
1094
|
|
|
1055
1095
|
// ../../shared/session/phase.ts
|
|
1096
|
+
var PHASES = [
|
|
1097
|
+
"idle",
|
|
1098
|
+
"running",
|
|
1099
|
+
"waiting_for_input",
|
|
1100
|
+
"finalizing",
|
|
1101
|
+
"review_listening",
|
|
1102
|
+
"completed",
|
|
1103
|
+
"superseded",
|
|
1104
|
+
"blocked",
|
|
1105
|
+
"failed",
|
|
1106
|
+
"stopped",
|
|
1107
|
+
"archived"
|
|
1108
|
+
];
|
|
1056
1109
|
var TERMINAL_PHASES_ARRAY = [
|
|
1057
1110
|
"completed",
|
|
1058
1111
|
"superseded",
|
|
@@ -1186,7 +1239,7 @@ async function resolveUploadedFileOptions(files) {
|
|
|
1186
1239
|
try {
|
|
1187
1240
|
return { name, content: await readFile(path, "utf8") };
|
|
1188
1241
|
} catch (err) {
|
|
1189
|
-
const message =
|
|
1242
|
+
const message = stringifyError(err);
|
|
1190
1243
|
throw new CliError("user", `Failed to read uploaded file ${path}: ${message}`);
|
|
1191
1244
|
}
|
|
1192
1245
|
})
|
|
@@ -2162,6 +2215,7 @@ var ERROR_CODES = [
|
|
|
2162
2215
|
"api_error",
|
|
2163
2216
|
"config_error",
|
|
2164
2217
|
"failed_edits",
|
|
2218
|
+
"memory_enforcement_failed",
|
|
2165
2219
|
"failure_loop_suspected",
|
|
2166
2220
|
"empty_completion",
|
|
2167
2221
|
"followup_not_started",
|
|
@@ -2202,6 +2256,7 @@ var ERROR_CODE_LABELS = {
|
|
|
2202
2256
|
api_error: "Provider API error",
|
|
2203
2257
|
config_error: "Configuration error",
|
|
2204
2258
|
failed_edits: "Edit failure",
|
|
2259
|
+
memory_enforcement_failed: "Memory enforcement failed",
|
|
2205
2260
|
failure_loop_suspected: "Suspected failure loop",
|
|
2206
2261
|
empty_completion: "Empty completion",
|
|
2207
2262
|
followup_not_started: "Follow-up did not start",
|
|
@@ -2245,19 +2300,7 @@ function formatSessionErrorMessage(error, code) {
|
|
|
2245
2300
|
// src/utils/session-output.ts
|
|
2246
2301
|
var SHORT_SESSION_ID_LENGTH = 8;
|
|
2247
2302
|
var PROMPT_LABEL_MAX_CHARS = 80;
|
|
2248
|
-
var VALID_PHASES =
|
|
2249
|
-
"idle",
|
|
2250
|
-
"running",
|
|
2251
|
-
"waiting_for_input",
|
|
2252
|
-
"finalizing",
|
|
2253
|
-
"review_listening",
|
|
2254
|
-
"completed",
|
|
2255
|
-
"superseded",
|
|
2256
|
-
"blocked",
|
|
2257
|
-
"failed",
|
|
2258
|
-
"stopped",
|
|
2259
|
-
"archived"
|
|
2260
|
-
]);
|
|
2303
|
+
var VALID_PHASES = new Set(PHASES);
|
|
2261
2304
|
var VALID_SANDBOX_SUBSTATES = /* @__PURE__ */ new Set(["creating", "reconnecting", "stopping", "none"]);
|
|
2262
2305
|
var VALID_STOP_MODES = /* @__PURE__ */ new Set(["user", "resumable", "none"]);
|
|
2263
2306
|
var VALID_FINALIZING_STEPS = /* @__PURE__ */ new Set(["post_execution", "publishing", "none"]);
|
|
@@ -2526,7 +2569,7 @@ function parseJsonObject(value) {
|
|
|
2526
2569
|
const parsed = JSON.parse(value);
|
|
2527
2570
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2528
2571
|
} catch (err) {
|
|
2529
|
-
throw new Error(`Malformed SSE JSON payload: ${
|
|
2572
|
+
throw new Error(`Malformed SSE JSON payload: ${stringifyError(err)}`);
|
|
2530
2573
|
}
|
|
2531
2574
|
}
|
|
2532
2575
|
function formatPromptLabel(promptId, promptLabels) {
|
|
@@ -2612,6 +2655,31 @@ function renderWatchEvent(event, state) {
|
|
|
2612
2655
|
}
|
|
2613
2656
|
}
|
|
2614
2657
|
|
|
2658
|
+
// src/commands/model-options.ts
|
|
2659
|
+
function resolveModelAndBackend(options) {
|
|
2660
|
+
let agentRuntimeBackend = CODEX_AGENT_RUNTIME_BACKEND;
|
|
2661
|
+
if (options.backend !== void 0) {
|
|
2662
|
+
if (!isAgentRuntimeBackend(options.backend)) {
|
|
2663
|
+
throw new CliError(
|
|
2664
|
+
"user",
|
|
2665
|
+
`Invalid --backend '${options.backend}'. Expected ${AGENT_RUNTIME_BACKENDS.join(", ")}.`
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
agentRuntimeBackend = options.backend;
|
|
2669
|
+
}
|
|
2670
|
+
if (options.model !== void 0) {
|
|
2671
|
+
const normalizedModel = extractModelId(options.model);
|
|
2672
|
+
if (normalizedModel === void 0 || !isSessionStartModelAllowedForBackend(normalizedModel, agentRuntimeBackend)) {
|
|
2673
|
+
const allowed = getSessionStartModelIdsForBackend(agentRuntimeBackend).join(", ");
|
|
2674
|
+
throw new CliError(
|
|
2675
|
+
"user",
|
|
2676
|
+
`Model '${options.model}' is not selectable for backend '${agentRuntimeBackend}'. Allowed: ${allowed}.`
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return agentRuntimeBackend;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2615
2683
|
// ../../shared/session/transient-disconnect.ts
|
|
2616
2684
|
var TRANSIENT_DISCONNECT_VISIBILITY_MS = 15e3;
|
|
2617
2685
|
function nextDisconnectMask(mask, previous, current, now) {
|
|
@@ -2706,9 +2774,7 @@ async function watchCommand(sessionId, options, command) {
|
|
|
2706
2774
|
try {
|
|
2707
2775
|
promptLabels = await fetchPromptLabels(config, sessionId);
|
|
2708
2776
|
} catch (err) {
|
|
2709
|
-
console.error(
|
|
2710
|
-
`Warning: failed to fetch prompt labels for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`
|
|
2711
|
-
);
|
|
2777
|
+
console.error(`Warning: failed to fetch prompt labels for session ${sessionId}: ${stringifyError(err)}`);
|
|
2712
2778
|
}
|
|
2713
2779
|
}
|
|
2714
2780
|
const renderState = {
|
|
@@ -2889,26 +2955,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
2889
2955
|
const runtime = getRuntimeOptions(command, options);
|
|
2890
2956
|
assertArcanistSessionMutationAllowed("create");
|
|
2891
2957
|
const config = requireConfig(runtime);
|
|
2892
|
-
|
|
2893
|
-
if (options.backend !== void 0) {
|
|
2894
|
-
if (!isAgentRuntimeBackend(options.backend)) {
|
|
2895
|
-
throw new CliError(
|
|
2896
|
-
"user",
|
|
2897
|
-
`Invalid --backend '${options.backend}'. Expected ${AGENT_RUNTIME_BACKENDS.join(", ")}.`
|
|
2898
|
-
);
|
|
2899
|
-
}
|
|
2900
|
-
agentRuntimeBackend = options.backend;
|
|
2901
|
-
}
|
|
2902
|
-
if (options.model !== void 0) {
|
|
2903
|
-
const normalizedModel = extractModelId(options.model);
|
|
2904
|
-
if (normalizedModel === void 0 || !isSessionStartModelAllowedForBackend(normalizedModel, agentRuntimeBackend)) {
|
|
2905
|
-
const allowed = getSessionStartModelIdsForBackend(agentRuntimeBackend).join(", ");
|
|
2906
|
-
throw new CliError(
|
|
2907
|
-
"user",
|
|
2908
|
-
`Model '${options.model}' is not selectable for backend '${agentRuntimeBackend}'. Allowed: ${allowed}.`
|
|
2909
|
-
);
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2958
|
+
const agentRuntimeBackend = resolveModelAndBackend(options);
|
|
2912
2959
|
const prompt = options.onboarding ? ONBOARDING_PROMPT : await resolvePromptInput(promptArg, options);
|
|
2913
2960
|
const waitPollIntervalMs = options.wait ? parsePollInterval(options.pollInterval) : null;
|
|
2914
2961
|
const repoError = validateRepoUrl(repoUrl);
|
|
@@ -2969,7 +3016,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
2969
3016
|
} catch (err) {
|
|
2970
3017
|
throw new CliError(
|
|
2971
3018
|
err instanceof CliError ? err.code : "server",
|
|
2972
|
-
`Session created (${sessionId}) but prompt failed: ${
|
|
3019
|
+
`Session created (${sessionId}) but prompt failed: ${stringifyError(err)}`,
|
|
2973
3020
|
{
|
|
2974
3021
|
exitCode: err instanceof CliError ? err.exitCode : void 0,
|
|
2975
3022
|
hint: `Retry with: arcanist sessions send ${sessionId} --prompt-stdin`,
|
|
@@ -3222,6 +3269,137 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
|
|
|
3222
3269
|
console.log(`Message sent to session ${sessionId}.`);
|
|
3223
3270
|
}
|
|
3224
3271
|
|
|
3272
|
+
// ../../shared/agent/verify-directive.ts
|
|
3273
|
+
var MAX_TARGET_PR_URL_LENGTH = 500;
|
|
3274
|
+
function normalizeGithubPullRequestUrl(rawUrl) {
|
|
3275
|
+
if (typeof rawUrl !== "string") return null;
|
|
3276
|
+
const trimmed = rawUrl.trim();
|
|
3277
|
+
if (!trimmed || trimmed.length > MAX_TARGET_PR_URL_LENGTH) return null;
|
|
3278
|
+
let url;
|
|
3279
|
+
try {
|
|
3280
|
+
url = new URL(trimmed);
|
|
3281
|
+
} catch {
|
|
3282
|
+
return null;
|
|
3283
|
+
}
|
|
3284
|
+
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "github.com") return null;
|
|
3285
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
3286
|
+
if (pathParts.length !== 4 || pathParts[2] !== "pull" || !/^\d+$/.test(pathParts[3])) return null;
|
|
3287
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(pathParts[0]) || !/^[A-Za-z0-9_.-]+$/.test(pathParts[1])) return null;
|
|
3288
|
+
return `https://github.com/${pathParts[0]}/${pathParts[1]}/pull/${pathParts[3]}`;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
// src/commands/qa.ts
|
|
3292
|
+
var QA_PROMPT = "Verify this pull request.";
|
|
3293
|
+
function parseCanonicalPrUrl(prUrl) {
|
|
3294
|
+
const targetPrUrl = normalizeGithubPullRequestUrl(prUrl);
|
|
3295
|
+
if (!targetPrUrl) {
|
|
3296
|
+
throw new CliError(
|
|
3297
|
+
"user",
|
|
3298
|
+
`Invalid pull request URL: "${prUrl}". Expected https://github.com/<owner>/<repo>/pull/<number>.`
|
|
3299
|
+
);
|
|
3300
|
+
}
|
|
3301
|
+
const url = new URL(targetPrUrl);
|
|
3302
|
+
const [owner, repo] = url.pathname.split("/").filter(Boolean);
|
|
3303
|
+
return {
|
|
3304
|
+
targetPrUrl,
|
|
3305
|
+
repoUrl: `https://github.com/${owner}/${repo}`
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
function parseCreateConflict(error) {
|
|
3309
|
+
let message = error.message;
|
|
3310
|
+
let sessionId;
|
|
3311
|
+
let sessionUrl;
|
|
3312
|
+
try {
|
|
3313
|
+
const parsed = JSON.parse(error.body);
|
|
3314
|
+
const parsedMessage = typeof parsed.error === "string" ? parsed.error : typeof parsed.error?.message === "string" ? parsed.error.message : void 0;
|
|
3315
|
+
if (parsedMessage) message = parsedMessage;
|
|
3316
|
+
if (typeof parsed.sessionId === "string") sessionId = parsed.sessionId;
|
|
3317
|
+
if (typeof parsed.sessionUrl === "string") sessionUrl = parsed.sessionUrl;
|
|
3318
|
+
} catch {
|
|
3319
|
+
}
|
|
3320
|
+
const location = sessionUrl ? ` (${sessionUrl})` : "";
|
|
3321
|
+
const existing = sessionId ? ` Existing verifier session: ${sessionId}${location}.` : "";
|
|
3322
|
+
return new CliError("conflict", `${message}${existing}`, {
|
|
3323
|
+
hint: error.hint,
|
|
3324
|
+
requestId: error.requestId
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
async function qaCommand(prUrl, options, command) {
|
|
3328
|
+
const runtime = getRuntimeOptions(command, options);
|
|
3329
|
+
assertArcanistSessionMutationAllowed("qa");
|
|
3330
|
+
const config = requireConfig(runtime);
|
|
3331
|
+
const { targetPrUrl, repoUrl } = parseCanonicalPrUrl(prUrl);
|
|
3332
|
+
const agentRuntimeBackend = resolveModelAndBackend(options);
|
|
3333
|
+
const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
|
|
3334
|
+
const sessionIdempotencyKey = `${idempotencyKey}:session`;
|
|
3335
|
+
const promptIdempotencyKey = `${idempotencyKey}:prompt`;
|
|
3336
|
+
const body = {
|
|
3337
|
+
context: { repoUrl },
|
|
3338
|
+
qa: true,
|
|
3339
|
+
targetPrUrl
|
|
3340
|
+
};
|
|
3341
|
+
if (options.model) body.model = options.model;
|
|
3342
|
+
if (options.backend) body.agentRuntimeBackend = agentRuntimeBackend;
|
|
3343
|
+
if (options.reasoningEffort) body.reasoningEffort = options.reasoningEffort;
|
|
3344
|
+
let sessionData;
|
|
3345
|
+
try {
|
|
3346
|
+
sessionData = await apiFetch(config, "/api/sessions", {
|
|
3347
|
+
method: "POST",
|
|
3348
|
+
headers: { "Idempotency-Key": sessionIdempotencyKey },
|
|
3349
|
+
body: JSON.stringify(body)
|
|
3350
|
+
});
|
|
3351
|
+
} catch (err) {
|
|
3352
|
+
if (err instanceof ApiError && err.status === 409) throw parseCreateConflict(err);
|
|
3353
|
+
throw err;
|
|
3354
|
+
}
|
|
3355
|
+
const sessionId = sessionData.sessionId;
|
|
3356
|
+
let promptId;
|
|
3357
|
+
try {
|
|
3358
|
+
const promptData = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
|
|
3359
|
+
method: "POST",
|
|
3360
|
+
headers: { "Idempotency-Key": promptIdempotencyKey },
|
|
3361
|
+
body: JSON.stringify({ prompt: QA_PROMPT })
|
|
3362
|
+
});
|
|
3363
|
+
promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
|
|
3364
|
+
} catch (err) {
|
|
3365
|
+
throw new CliError(
|
|
3366
|
+
err instanceof CliError ? err.code : "server",
|
|
3367
|
+
`QA session created (${sessionId}) but prompt enqueue failed: ${stringifyError(err)}`,
|
|
3368
|
+
{
|
|
3369
|
+
exitCode: err instanceof CliError ? err.exitCode : void 0,
|
|
3370
|
+
hint: `Retry with: arcanist sessions qa ${targetPrUrl} --idempotency-key ${idempotencyKey}`,
|
|
3371
|
+
requestId: err instanceof CliError ? err.requestId : void 0
|
|
3372
|
+
}
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
if (options.wait) {
|
|
3376
|
+
const waitPollIntervalMs = parsePollInterval(options.pollInterval);
|
|
3377
|
+
if (!isJson(command, options)) {
|
|
3378
|
+
console.log(`Session: ${sessionId}`);
|
|
3379
|
+
if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
|
|
3380
|
+
}
|
|
3381
|
+
await waitForCreatedPrompt(sessionId, promptId, sessionData.sessionUrl, waitPollIntervalMs, runtime, command);
|
|
3382
|
+
if (!isJson(command, options)) {
|
|
3383
|
+
console.log(`Target PR: ${targetPrUrl}`);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
if (isJson(command, options)) {
|
|
3387
|
+
const output = { sessionId, repoUrl, targetPrUrl };
|
|
3388
|
+
if (sessionData.sessionUrl) output.sessionUrl = sessionData.sessionUrl;
|
|
3389
|
+
if (options.model) output.model = options.model;
|
|
3390
|
+
if (options.backend) output.agentRuntimeBackend = agentRuntimeBackend;
|
|
3391
|
+
if (options.reasoningEffort) output.reasoningEffort = options.reasoningEffort;
|
|
3392
|
+
if (promptId) output.promptId = promptId;
|
|
3393
|
+
writeJson(output);
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
if (options.wait) return;
|
|
3397
|
+
console.log(`Session: ${sessionId}`);
|
|
3398
|
+
if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
|
|
3399
|
+
console.log(`Target PR: ${targetPrUrl}`);
|
|
3400
|
+
console.log(`Follow with: arcanist sessions events ${sessionId} --follow --json`);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3225
3403
|
// src/commands/sandbox.ts
|
|
3226
3404
|
import { execFileSync } from "child_process";
|
|
3227
3405
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -3763,7 +3941,7 @@ function git(args) {
|
|
|
3763
3941
|
try {
|
|
3764
3942
|
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3765
3943
|
} catch (err) {
|
|
3766
|
-
throw new CliError("user", `git ${args.join(" ")} failed: ${
|
|
3944
|
+
throw new CliError("user", `git ${args.join(" ")} failed: ${stringifyError(err)}`);
|
|
3767
3945
|
}
|
|
3768
3946
|
}
|
|
3769
3947
|
function currentRepo() {
|
|
@@ -4346,9 +4524,7 @@ async function sessionEventsCommand(sessionId, options, command) {
|
|
|
4346
4524
|
return;
|
|
4347
4525
|
}
|
|
4348
4526
|
const promptLabels = await fetchPromptLabels(config, sessionId).catch((err) => {
|
|
4349
|
-
console.error(
|
|
4350
|
-
`Warning: failed to fetch prompt labels for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`
|
|
4351
|
-
);
|
|
4527
|
+
console.error(`Warning: failed to fetch prompt labels for session ${sessionId}: ${stringifyError(err)}`);
|
|
4352
4528
|
return /* @__PURE__ */ new Map();
|
|
4353
4529
|
});
|
|
4354
4530
|
const state = { promptLabels, toolCalls: /* @__PURE__ */ new Map() };
|
|
@@ -4643,6 +4819,25 @@ JSON mode returns {sessionId, promptId?}
|
|
|
4643
4819
|
`
|
|
4644
4820
|
);
|
|
4645
4821
|
}
|
|
4822
|
+
function addQaOptions(cmd) {
|
|
4823
|
+
return cmd.argument("<pr-url>", "GitHub pull request URL to QA").option("--model <model>", "Model to use").option("--backend <backend>", "Agent runtime backend: codex (default), claude_code, or opencode").option("--reasoning-effort <effort>", "Reasoning effort to use for models that support it").option("--wait", "Wait for the QA prompt to finish and exit non-zero if it fails").option(
|
|
4824
|
+
"--poll-interval <ms>",
|
|
4825
|
+
"Polling interval in milliseconds while waiting",
|
|
4826
|
+
String(DEFAULT_WATCH_POLL_INTERVAL_MS)
|
|
4827
|
+
).option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText(
|
|
4828
|
+
"after",
|
|
4829
|
+
`
|
|
4830
|
+
Examples:
|
|
4831
|
+
arcanist sessions qa https://github.com/org/repo/pull/123
|
|
4832
|
+
arcanist sessions qa https://github.com/org/repo/pull/123 --model gpt-5.4 --wait
|
|
4833
|
+
arcanist sessions qa https://github.com/org/repo/pull/123 --idempotency-key 1f0e6f1a-...
|
|
4834
|
+
|
|
4835
|
+
JSON:
|
|
4836
|
+
JSON mode returns {sessionId, sessionUrl?, repoUrl, targetPrUrl, model?, agentRuntimeBackend?, reasoningEffort?, promptId?}
|
|
4837
|
+
Inspect progress with the session URL or the session events stream.
|
|
4838
|
+
`
|
|
4839
|
+
);
|
|
4840
|
+
}
|
|
4646
4841
|
var auth = program.command("auth").description("Authentication commands");
|
|
4647
4842
|
auth.command("login").description("Authenticate with a personal access token").option("--token-stdin", "Read token from stdin instead of interactive prompt").option("--api-url <url>", "Set custom API URL").addHelpText(
|
|
4648
4843
|
"after",
|
|
@@ -4668,6 +4863,9 @@ addCreateOptions(sessions.command("create").description("Create a session and se
|
|
|
4668
4863
|
addSendOptions(sessions.command("send").description("Send a message to an existing session")).action(
|
|
4669
4864
|
(sessionId, prompt, options, command) => messageCommand(sessionId, prompt, options, command)
|
|
4670
4865
|
);
|
|
4866
|
+
addQaOptions(sessions.command("qa").description("Start a QA verification session for a GitHub pull request")).action(
|
|
4867
|
+
(prUrl, options, command) => qaCommand(prUrl, options, command)
|
|
4868
|
+
);
|
|
4671
4869
|
sessions.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").addHelpText(
|
|
4672
4870
|
"after",
|
|
4673
4871
|
`
|