@tryarcanist/cli 0.1.166 → 0.1.168
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 +46 -2
- package/dist/index.js +295 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,9 +53,12 @@ Supported environment variables:
|
|
|
53
53
|
```bash
|
|
54
54
|
export ARCANIST_TOKEN=arc_...
|
|
55
55
|
export ARCANIST_API_URL=https://api.tryarcanist.com
|
|
56
|
+
export ARCANIST_HTTP_TIMEOUT_MS=30000
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
`--token` and `--api-url` are available for one-off invocations. Prefer `ARCANIST_TOKEN` over `--token` for persistent use; CLI flags can appear in shell history and process lists.
|
|
60
|
+
Each HTTP request times out after `ARCANIST_HTTP_TIMEOUT_MS` milliseconds, default `30000`.
|
|
61
|
+
The value must be between `1000` and `600000`; invalid values fail closed instead of falling back silently.
|
|
59
62
|
|
|
60
63
|
Non-local API URLs must use HTTPS and must not embed credentials.
|
|
61
64
|
|
|
@@ -89,10 +92,18 @@ With `--json`, successful command output is machine-readable and newline-termina
|
|
|
89
92
|
|
|
90
93
|
```json
|
|
91
94
|
{
|
|
92
|
-
"error": {
|
|
95
|
+
"error": {
|
|
96
|
+
"code": "auth",
|
|
97
|
+
"message": "Not logged in.",
|
|
98
|
+
"hint": "Run `arcanist auth login` or set `ARCANIST_TOKEN`.",
|
|
99
|
+
"data": { "serverCode": "token_revoked" }
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
```
|
|
95
103
|
|
|
104
|
+
`error.data` is omitted when there is no structured recovery data.
|
|
105
|
+
Stable fields are `serverCode` for server-provided error codes and `sessionId`/`sessionUrl` when `sessions create` created the session but failed to enqueue the prompt.
|
|
106
|
+
|
|
96
107
|
Exit codes:
|
|
97
108
|
|
|
98
109
|
```text
|
|
@@ -159,12 +170,45 @@ Repeatable `--uploaded-file <path>` flags attach local UTF-8 text files to the p
|
|
|
159
170
|
|
|
160
171
|
`--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
172
|
|
|
162
|
-
`--idempotency-key <uuid>` is for manually retrying a create request that may have reached the server.
|
|
173
|
+
`--idempotency-key <uuid>` is for manually retrying a create request that may have reached the server.
|
|
174
|
+
The CLI derives separate session and prompt idempotency keys from the provided value.
|
|
163
175
|
|
|
164
176
|
`--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
177
|
|
|
166
178
|
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
179
|
|
|
180
|
+
### `arcanist sessions qa <pr-url>`
|
|
181
|
+
|
|
182
|
+
Starts a QA verification session for an existing GitHub pull request.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123
|
|
186
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --model gpt-5.4
|
|
187
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --backend claude_code --model claude-opus-4-8
|
|
188
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --reasoning-effort xhigh
|
|
189
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --wait
|
|
190
|
+
arcanist sessions qa https://github.com/your-org/your-repo/pull/123 --idempotency-key 1f0e6f1a-...
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The PR URL must be `https://github.com/<owner>/<repo>/pull/<number>`.
|
|
194
|
+
The CLI derives the repo from that URL, creates a QA session with `qa: true` and `targetPrUrl`, then enqueues the verifier prompt.
|
|
195
|
+
Inspect progress with the session URL or the session events stream.
|
|
196
|
+
|
|
197
|
+
`--backend` picks the agent runtime backend: `codex` (default), `claude_code`, or `opencode`.
|
|
198
|
+
`--model` must be valid for the chosen backend, and the CLI rejects mismatches before any network call.
|
|
199
|
+
When `--model` is omitted, the backend default is used.
|
|
200
|
+
|
|
201
|
+
`--wait` blocks until the enqueued QA prompt finishes and exits non-zero if the prompt fails.
|
|
202
|
+
JSON mode waits quietly and prints the create payload after the prompt completes successfully.
|
|
203
|
+
`--poll-interval <ms>` tunes completion polling.
|
|
204
|
+
|
|
205
|
+
`--idempotency-key <uuid>` is for manually retrying a QA request that may have reached the server.
|
|
206
|
+
The CLI derives separate session and prompt idempotency keys from the provided value.
|
|
207
|
+
If session creation succeeds but prompt enqueue fails, retry the same command with the same `--idempotency-key`.
|
|
208
|
+
|
|
209
|
+
JSON mode returns `{sessionId, sessionUrl?, repoUrl, targetPrUrl, model?, agentRuntimeBackend?, reasoningEffort?, promptId?}`.
|
|
210
|
+
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.
|
|
211
|
+
|
|
168
212
|
### `arcanist sessions send <session-id> [prompt]`
|
|
169
213
|
|
|
170
214
|
Sends a follow-up message to an existing session.
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ var CliError = class extends Error {
|
|
|
24
24
|
exitCode;
|
|
25
25
|
hint;
|
|
26
26
|
requestId;
|
|
27
|
+
data;
|
|
27
28
|
constructor(code, message, options = {}) {
|
|
28
29
|
super(message);
|
|
29
30
|
this.name = "CliError";
|
|
@@ -31,17 +32,20 @@ var CliError = class extends Error {
|
|
|
31
32
|
this.exitCode = options.exitCode ?? exitCodeForErrorCode(code);
|
|
32
33
|
this.hint = options.hint;
|
|
33
34
|
this.requestId = options.requestId;
|
|
35
|
+
this.data = options.data;
|
|
34
36
|
}
|
|
35
37
|
};
|
|
36
38
|
var ApiError = class extends CliError {
|
|
37
39
|
status;
|
|
38
40
|
body;
|
|
39
|
-
constructor(status, body, requestId) {
|
|
41
|
+
constructor(status, body, requestId, resourceNoun) {
|
|
40
42
|
const code = codeForHttpStatus(status);
|
|
41
|
-
|
|
43
|
+
const parsed = parseApiErrorBody(body);
|
|
44
|
+
super(code, messageForApiError(status, body, parsed), {
|
|
42
45
|
exitCode: exitCodeForHttpStatus(status),
|
|
43
|
-
hint: hintForHttpStatus(status),
|
|
44
|
-
requestId
|
|
46
|
+
hint: hintForHttpStatus(status, resourceNoun),
|
|
47
|
+
requestId,
|
|
48
|
+
data: parsed?.serverCode ? { serverCode: parsed.serverCode } : void 0
|
|
45
49
|
});
|
|
46
50
|
this.name = "ApiError";
|
|
47
51
|
this.status = status;
|
|
@@ -73,9 +77,9 @@ function codeForHttpStatus(status) {
|
|
|
73
77
|
function exitCodeForHttpStatus(status) {
|
|
74
78
|
return exitCodeForErrorCode(codeForHttpStatus(status));
|
|
75
79
|
}
|
|
76
|
-
function messageForApiError(status, body) {
|
|
77
|
-
|
|
78
|
-
if (parsed) return parsed;
|
|
80
|
+
function messageForApiError(status, body, parsed = parseApiErrorBody(body)) {
|
|
81
|
+
if (parsed?.message) return parsed.message;
|
|
82
|
+
if (parsed?.serverCode) return parsed.serverCode;
|
|
79
83
|
return body ? `API error ${status}: ${body}` : `API error ${status}`;
|
|
80
84
|
}
|
|
81
85
|
function parseApiErrorBody(body) {
|
|
@@ -83,14 +87,36 @@ function parseApiErrorBody(body) {
|
|
|
83
87
|
const parsed = JSON.parse(body);
|
|
84
88
|
if (!parsed || typeof parsed !== "object") return null;
|
|
85
89
|
const error = parsed.error;
|
|
86
|
-
|
|
90
|
+
if (typeof error === "string" && error.length > 0) {
|
|
91
|
+
const message2 = parsed.message;
|
|
92
|
+
const code = parsed.code;
|
|
93
|
+
return {
|
|
94
|
+
serverCode: typeof code === "string" && code ? code : error,
|
|
95
|
+
...typeof message2 === "string" && message2 ? { message: message2 } : { message: error }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (error && typeof error === "object") {
|
|
99
|
+
const code = error.code;
|
|
100
|
+
const message2 = error.message;
|
|
101
|
+
return {
|
|
102
|
+
...typeof message2 === "string" && message2 ? { message: message2 } : {},
|
|
103
|
+
...typeof code === "string" && code ? { serverCode: code } : {}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const message = parsed.message;
|
|
107
|
+
return typeof message === "string" && message.length > 0 ? { message } : null;
|
|
87
108
|
} catch {
|
|
88
109
|
return null;
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
|
-
function hintForHttpStatus(status) {
|
|
112
|
+
function hintForHttpStatus(status, resourceNoun) {
|
|
92
113
|
if (status === 401 || status === 403) return "Run `arcanist auth login` or set `ARCANIST_TOKEN`.";
|
|
93
|
-
if (status === 404)
|
|
114
|
+
if (status === 404) {
|
|
115
|
+
if (resourceNoun === "token") return "List tokens with `arcanist tokens list`.";
|
|
116
|
+
if (resourceNoun === "sandbox") return "Check sandbox state with `arcanist sandbox status`.";
|
|
117
|
+
if (resourceNoun === "repo") return "Check accessible repositories with `arcanist repos list`.";
|
|
118
|
+
return "List sessions with `arcanist sessions list`.";
|
|
119
|
+
}
|
|
94
120
|
if (status === 409) return "Check the current resource state and retry the command when it is ready.";
|
|
95
121
|
if (status >= 500) return "Retry later or check the control-plane logs.";
|
|
96
122
|
return void 0;
|
|
@@ -106,7 +132,8 @@ function formatJsonError(err) {
|
|
|
106
132
|
code: err.code,
|
|
107
133
|
message: err.message,
|
|
108
134
|
...err.hint ? { hint: err.hint } : {},
|
|
109
|
-
...err.requestId ? { requestId: err.requestId } : {}
|
|
135
|
+
...err.requestId ? { requestId: err.requestId } : {},
|
|
136
|
+
...err.data ? { data: err.data } : {}
|
|
110
137
|
}
|
|
111
138
|
});
|
|
112
139
|
}
|
|
@@ -115,33 +142,72 @@ function formatJsonError(err) {
|
|
|
115
142
|
var require2 = createRequire(import.meta.url);
|
|
116
143
|
var { version } = require2("../package.json");
|
|
117
144
|
var CLI_USER_AGENT = `arcanist-cli/${version}`;
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
var DEFAULT_HTTP_TIMEOUT_MS = 3e4;
|
|
146
|
+
var MIN_HTTP_TIMEOUT_MS = 1e3;
|
|
147
|
+
var MAX_HTTP_TIMEOUT_MS = 6e5;
|
|
148
|
+
var HTTP_TIMEOUT_ENV = "ARCANIST_HTTP_TIMEOUT_MS";
|
|
149
|
+
function parseHttpTimeoutMs(env = process.env) {
|
|
150
|
+
const raw = env[HTTP_TIMEOUT_ENV];
|
|
151
|
+
if (!raw) return DEFAULT_HTTP_TIMEOUT_MS;
|
|
152
|
+
if (!/^\d+$/.test(raw)) {
|
|
153
|
+
throw new CliError("user", `${HTTP_TIMEOUT_ENV} must be a positive integer between 1000 and 600000.`);
|
|
154
|
+
}
|
|
155
|
+
const value = Number(raw);
|
|
156
|
+
if (value < MIN_HTTP_TIMEOUT_MS || value > MAX_HTTP_TIMEOUT_MS) {
|
|
157
|
+
throw new CliError("user", `${HTTP_TIMEOUT_ENV} must be between 1000 and 600000 milliseconds.`);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
function isAbortError(err) {
|
|
162
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
163
|
+
}
|
|
164
|
+
function timeoutError(timeoutMs) {
|
|
165
|
+
return new CliError("server", `Network timeout after ${timeoutMs}ms.`, {
|
|
166
|
+
hint: `Increase ${HTTP_TIMEOUT_ENV} for slow links, or check the API connection.`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function apiRequest(config, path, init, read) {
|
|
170
|
+
const timeoutMs = parseHttpTimeoutMs();
|
|
171
|
+
const controller = new AbortController();
|
|
172
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
120
173
|
try {
|
|
121
174
|
const headers = new Headers(init?.headers);
|
|
122
175
|
headers.set("Content-Type", "application/json");
|
|
123
176
|
headers.set("User-Agent", CLI_USER_AGENT);
|
|
124
177
|
headers.set("Authorization", `Bearer ${config.token}`);
|
|
125
|
-
res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
|
|
178
|
+
const res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
|
|
126
179
|
...init,
|
|
127
|
-
headers
|
|
180
|
+
headers,
|
|
181
|
+
signal: controller.signal
|
|
128
182
|
});
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const body = await res.text().catch((err) => {
|
|
185
|
+
if (isAbortError(err)) throw timeoutError(timeoutMs);
|
|
186
|
+
return "";
|
|
187
|
+
});
|
|
188
|
+
throw new ApiError(res.status, body, res.headers.get("x-request-id") ?? void 0, resourceNounForPath(path));
|
|
189
|
+
}
|
|
190
|
+
return await read(res);
|
|
129
191
|
} catch (err) {
|
|
192
|
+
if (err instanceof CliError) throw err;
|
|
193
|
+
if (isAbortError(err)) throw timeoutError(timeoutMs);
|
|
130
194
|
throw new CliError("server", `Network error: ${stringifyError(err)}`);
|
|
195
|
+
} finally {
|
|
196
|
+
clearTimeout(timeout);
|
|
131
197
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return
|
|
198
|
+
}
|
|
199
|
+
function resourceNounForPath(path) {
|
|
200
|
+
if (path.startsWith("/api/cli-tokens")) return "token";
|
|
201
|
+
if (path.startsWith("/api/repos")) return "repo";
|
|
202
|
+
if (path.startsWith("/api/sandbox")) return "sandbox";
|
|
203
|
+
if (path.startsWith("/api/sessions")) return "session";
|
|
204
|
+
return void 0;
|
|
137
205
|
}
|
|
138
206
|
async function apiFetch(config, path, init) {
|
|
139
|
-
|
|
140
|
-
return res.json();
|
|
207
|
+
return apiRequest(config, path, init, (res) => res.json());
|
|
141
208
|
}
|
|
142
209
|
async function apiFetchText(config, path, init) {
|
|
143
|
-
|
|
144
|
-
return res.text();
|
|
210
|
+
return apiRequest(config, path, init, (res) => res.text());
|
|
145
211
|
}
|
|
146
212
|
async function resolveBusinessId(config, options) {
|
|
147
213
|
if (options.business && options.business.trim()) return options.business.trim();
|
|
@@ -1126,8 +1192,9 @@ function isTerminalPhase(phase, _sessionKind) {
|
|
|
1126
1192
|
}
|
|
1127
1193
|
|
|
1128
1194
|
// src/constants/watch.ts
|
|
1129
|
-
var MIN_WATCH_POLL_INTERVAL_MS =
|
|
1195
|
+
var MIN_WATCH_POLL_INTERVAL_MS = 250;
|
|
1130
1196
|
var DEFAULT_WATCH_POLL_INTERVAL_MS = 1e3;
|
|
1197
|
+
var MAX_WATCH_POLL_INTERVAL_MS = 6e4;
|
|
1131
1198
|
var WATCH_REPLAY_PAGE_SIZE = 200;
|
|
1132
1199
|
function isWatchTerminal(phase) {
|
|
1133
1200
|
return isTerminalPhase(phase);
|
|
@@ -2655,6 +2722,31 @@ function renderWatchEvent(event, state) {
|
|
|
2655
2722
|
}
|
|
2656
2723
|
}
|
|
2657
2724
|
|
|
2725
|
+
// src/commands/model-options.ts
|
|
2726
|
+
function resolveModelAndBackend(options) {
|
|
2727
|
+
let agentRuntimeBackend = CODEX_AGENT_RUNTIME_BACKEND;
|
|
2728
|
+
if (options.backend !== void 0) {
|
|
2729
|
+
if (!isAgentRuntimeBackend(options.backend)) {
|
|
2730
|
+
throw new CliError(
|
|
2731
|
+
"user",
|
|
2732
|
+
`Invalid --backend '${options.backend}'. Expected ${AGENT_RUNTIME_BACKENDS.join(", ")}.`
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
agentRuntimeBackend = options.backend;
|
|
2736
|
+
}
|
|
2737
|
+
if (options.model !== void 0) {
|
|
2738
|
+
const normalizedModel = extractModelId(options.model);
|
|
2739
|
+
if (normalizedModel === void 0 || !isSessionStartModelAllowedForBackend(normalizedModel, agentRuntimeBackend)) {
|
|
2740
|
+
const allowed = getSessionStartModelIdsForBackend(agentRuntimeBackend).join(", ");
|
|
2741
|
+
throw new CliError(
|
|
2742
|
+
"user",
|
|
2743
|
+
`Model '${options.model}' is not selectable for backend '${agentRuntimeBackend}'. Allowed: ${allowed}.`
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return agentRuntimeBackend;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2658
2750
|
// ../../shared/session/transient-disconnect.ts
|
|
2659
2751
|
var TRANSIENT_DISCONNECT_VISIBILITY_MS = 15e3;
|
|
2660
2752
|
function nextDisconnectMask(mask, previous, current, now) {
|
|
@@ -2677,7 +2769,14 @@ function parsePollInterval(raw) {
|
|
|
2677
2769
|
if (!/^\d+$/.test(raw)) {
|
|
2678
2770
|
throw new CliError("user", "Polling interval must be a non-negative integer.");
|
|
2679
2771
|
}
|
|
2680
|
-
|
|
2772
|
+
const value = Number(raw);
|
|
2773
|
+
if (value < MIN_WATCH_POLL_INTERVAL_MS || value > MAX_WATCH_POLL_INTERVAL_MS) {
|
|
2774
|
+
throw new CliError(
|
|
2775
|
+
"user",
|
|
2776
|
+
`Polling interval must be between ${MIN_WATCH_POLL_INTERVAL_MS} and ${MAX_WATCH_POLL_INTERVAL_MS} milliseconds.`
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
return value;
|
|
2681
2780
|
}
|
|
2682
2781
|
function parseNonNegativeInteger(raw, name, defaultValue) {
|
|
2683
2782
|
if (!raw) return defaultValue;
|
|
@@ -2737,7 +2836,6 @@ async function watchCommand(sessionId, options, command) {
|
|
|
2737
2836
|
const runtime = getRuntimeOptions(command, options);
|
|
2738
2837
|
const config = requireConfig(runtime);
|
|
2739
2838
|
const pollIntervalMs = parsePollInterval(options.pollInterval);
|
|
2740
|
-
const effectivePollIntervalMs = Math.max(pollIntervalMs, MIN_WATCH_POLL_INTERVAL_MS);
|
|
2741
2839
|
const initialAfterSequence = parseNonNegativeInteger(options.afterSequence, "--after-sequence", 0);
|
|
2742
2840
|
const pageSize = parseNonNegativeInteger(options.limit, "--limit", WATCH_REPLAY_PAGE_SIZE);
|
|
2743
2841
|
if (pageSize === 0) {
|
|
@@ -2820,7 +2918,7 @@ async function watchCommand(sessionId, options, command) {
|
|
|
2820
2918
|
}
|
|
2821
2919
|
break;
|
|
2822
2920
|
}
|
|
2823
|
-
await sleep(
|
|
2921
|
+
await sleep(pollIntervalMs);
|
|
2824
2922
|
}
|
|
2825
2923
|
} finally {
|
|
2826
2924
|
if (textOpen) process.stdout.write("\n");
|
|
@@ -2930,26 +3028,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
2930
3028
|
const runtime = getRuntimeOptions(command, options);
|
|
2931
3029
|
assertArcanistSessionMutationAllowed("create");
|
|
2932
3030
|
const config = requireConfig(runtime);
|
|
2933
|
-
|
|
2934
|
-
if (options.backend !== void 0) {
|
|
2935
|
-
if (!isAgentRuntimeBackend(options.backend)) {
|
|
2936
|
-
throw new CliError(
|
|
2937
|
-
"user",
|
|
2938
|
-
`Invalid --backend '${options.backend}'. Expected ${AGENT_RUNTIME_BACKENDS.join(", ")}.`
|
|
2939
|
-
);
|
|
2940
|
-
}
|
|
2941
|
-
agentRuntimeBackend = options.backend;
|
|
2942
|
-
}
|
|
2943
|
-
if (options.model !== void 0) {
|
|
2944
|
-
const normalizedModel = extractModelId(options.model);
|
|
2945
|
-
if (normalizedModel === void 0 || !isSessionStartModelAllowedForBackend(normalizedModel, agentRuntimeBackend)) {
|
|
2946
|
-
const allowed = getSessionStartModelIdsForBackend(agentRuntimeBackend).join(", ");
|
|
2947
|
-
throw new CliError(
|
|
2948
|
-
"user",
|
|
2949
|
-
`Model '${options.model}' is not selectable for backend '${agentRuntimeBackend}'. Allowed: ${allowed}.`
|
|
2950
|
-
);
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
3031
|
+
const agentRuntimeBackend = resolveModelAndBackend(options);
|
|
2953
3032
|
const prompt = options.onboarding ? ONBOARDING_PROMPT : await resolvePromptInput(promptArg, options);
|
|
2954
3033
|
const waitPollIntervalMs = options.wait ? parsePollInterval(options.pollInterval) : null;
|
|
2955
3034
|
const repoError = validateRepoUrl(repoUrl);
|
|
@@ -3014,7 +3093,12 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
3014
3093
|
{
|
|
3015
3094
|
exitCode: err instanceof CliError ? err.exitCode : void 0,
|
|
3016
3095
|
hint: `Retry with: arcanist sessions send ${sessionId} --prompt-stdin`,
|
|
3017
|
-
requestId: err instanceof CliError ? err.requestId : void 0
|
|
3096
|
+
requestId: err instanceof CliError ? err.requestId : void 0,
|
|
3097
|
+
data: {
|
|
3098
|
+
...err instanceof CliError && err.data ? err.data : {},
|
|
3099
|
+
sessionId,
|
|
3100
|
+
...sessionData.sessionUrl ? { sessionUrl: sessionData.sessionUrl } : {}
|
|
3101
|
+
}
|
|
3018
3102
|
}
|
|
3019
3103
|
);
|
|
3020
3104
|
}
|
|
@@ -3226,16 +3310,16 @@ async function loginCommand(options, command) {
|
|
|
3226
3310
|
console.log(`Logged in. API: ${apiUrl}`);
|
|
3227
3311
|
}
|
|
3228
3312
|
try {
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
if (
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3313
|
+
await apiFetch({ apiUrl, token }, "/api/cli-tokens");
|
|
3314
|
+
if (!runtime.json && !runtime.quiet) console.log("Token verified.");
|
|
3315
|
+
} catch (err) {
|
|
3316
|
+
if (!runtime.json && !runtime.quiet) {
|
|
3317
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
3318
|
+
console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
|
|
3319
|
+
} else {
|
|
3320
|
+
console.warn("Warning: Could not reach API to verify token.");
|
|
3321
|
+
}
|
|
3236
3322
|
}
|
|
3237
|
-
} catch {
|
|
3238
|
-
if (!runtime.json && !runtime.quiet) console.warn("Warning: Could not reach API to verify token.");
|
|
3239
3323
|
}
|
|
3240
3324
|
}
|
|
3241
3325
|
|
|
@@ -3263,6 +3347,137 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
|
|
|
3263
3347
|
console.log(`Message sent to session ${sessionId}.`);
|
|
3264
3348
|
}
|
|
3265
3349
|
|
|
3350
|
+
// ../../shared/agent/verify-directive.ts
|
|
3351
|
+
var MAX_TARGET_PR_URL_LENGTH = 500;
|
|
3352
|
+
function normalizeGithubPullRequestUrl(rawUrl) {
|
|
3353
|
+
if (typeof rawUrl !== "string") return null;
|
|
3354
|
+
const trimmed = rawUrl.trim();
|
|
3355
|
+
if (!trimmed || trimmed.length > MAX_TARGET_PR_URL_LENGTH) return null;
|
|
3356
|
+
let url;
|
|
3357
|
+
try {
|
|
3358
|
+
url = new URL(trimmed);
|
|
3359
|
+
} catch {
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3362
|
+
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "github.com") return null;
|
|
3363
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
3364
|
+
if (pathParts.length !== 4 || pathParts[2] !== "pull" || !/^\d+$/.test(pathParts[3])) return null;
|
|
3365
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(pathParts[0]) || !/^[A-Za-z0-9_.-]+$/.test(pathParts[1])) return null;
|
|
3366
|
+
return `https://github.com/${pathParts[0]}/${pathParts[1]}/pull/${pathParts[3]}`;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
// src/commands/qa.ts
|
|
3370
|
+
var QA_PROMPT = "Verify this pull request.";
|
|
3371
|
+
function parseCanonicalPrUrl(prUrl) {
|
|
3372
|
+
const targetPrUrl = normalizeGithubPullRequestUrl(prUrl);
|
|
3373
|
+
if (!targetPrUrl) {
|
|
3374
|
+
throw new CliError(
|
|
3375
|
+
"user",
|
|
3376
|
+
`Invalid pull request URL: "${prUrl}". Expected https://github.com/<owner>/<repo>/pull/<number>.`
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
const url = new URL(targetPrUrl);
|
|
3380
|
+
const [owner, repo] = url.pathname.split("/").filter(Boolean);
|
|
3381
|
+
return {
|
|
3382
|
+
targetPrUrl,
|
|
3383
|
+
repoUrl: `https://github.com/${owner}/${repo}`
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
function parseCreateConflict(error) {
|
|
3387
|
+
let message = error.message;
|
|
3388
|
+
let sessionId;
|
|
3389
|
+
let sessionUrl;
|
|
3390
|
+
try {
|
|
3391
|
+
const parsed = JSON.parse(error.body);
|
|
3392
|
+
const parsedMessage = typeof parsed.error === "string" ? parsed.error : typeof parsed.error?.message === "string" ? parsed.error.message : void 0;
|
|
3393
|
+
if (parsedMessage) message = parsedMessage;
|
|
3394
|
+
if (typeof parsed.sessionId === "string") sessionId = parsed.sessionId;
|
|
3395
|
+
if (typeof parsed.sessionUrl === "string") sessionUrl = parsed.sessionUrl;
|
|
3396
|
+
} catch {
|
|
3397
|
+
}
|
|
3398
|
+
const location = sessionUrl ? ` (${sessionUrl})` : "";
|
|
3399
|
+
const existing = sessionId ? ` Existing verifier session: ${sessionId}${location}.` : "";
|
|
3400
|
+
return new CliError("conflict", `${message}${existing}`, {
|
|
3401
|
+
hint: error.hint,
|
|
3402
|
+
requestId: error.requestId
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
async function qaCommand(prUrl, options, command) {
|
|
3406
|
+
const runtime = getRuntimeOptions(command, options);
|
|
3407
|
+
assertArcanistSessionMutationAllowed("qa");
|
|
3408
|
+
const config = requireConfig(runtime);
|
|
3409
|
+
const { targetPrUrl, repoUrl } = parseCanonicalPrUrl(prUrl);
|
|
3410
|
+
const agentRuntimeBackend = resolveModelAndBackend(options);
|
|
3411
|
+
const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
|
|
3412
|
+
const sessionIdempotencyKey = `${idempotencyKey}:session`;
|
|
3413
|
+
const promptIdempotencyKey = `${idempotencyKey}:prompt`;
|
|
3414
|
+
const body = {
|
|
3415
|
+
context: { repoUrl },
|
|
3416
|
+
qa: true,
|
|
3417
|
+
targetPrUrl
|
|
3418
|
+
};
|
|
3419
|
+
if (options.model) body.model = options.model;
|
|
3420
|
+
if (options.backend) body.agentRuntimeBackend = agentRuntimeBackend;
|
|
3421
|
+
if (options.reasoningEffort) body.reasoningEffort = options.reasoningEffort;
|
|
3422
|
+
let sessionData;
|
|
3423
|
+
try {
|
|
3424
|
+
sessionData = await apiFetch(config, "/api/sessions", {
|
|
3425
|
+
method: "POST",
|
|
3426
|
+
headers: { "Idempotency-Key": sessionIdempotencyKey },
|
|
3427
|
+
body: JSON.stringify(body)
|
|
3428
|
+
});
|
|
3429
|
+
} catch (err) {
|
|
3430
|
+
if (err instanceof ApiError && err.status === 409) throw parseCreateConflict(err);
|
|
3431
|
+
throw err;
|
|
3432
|
+
}
|
|
3433
|
+
const sessionId = sessionData.sessionId;
|
|
3434
|
+
let promptId;
|
|
3435
|
+
try {
|
|
3436
|
+
const promptData = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
|
|
3437
|
+
method: "POST",
|
|
3438
|
+
headers: { "Idempotency-Key": promptIdempotencyKey },
|
|
3439
|
+
body: JSON.stringify({ prompt: QA_PROMPT })
|
|
3440
|
+
});
|
|
3441
|
+
promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
|
|
3442
|
+
} catch (err) {
|
|
3443
|
+
throw new CliError(
|
|
3444
|
+
err instanceof CliError ? err.code : "server",
|
|
3445
|
+
`QA session created (${sessionId}) but prompt enqueue failed: ${stringifyError(err)}`,
|
|
3446
|
+
{
|
|
3447
|
+
exitCode: err instanceof CliError ? err.exitCode : void 0,
|
|
3448
|
+
hint: `Retry with: arcanist sessions qa ${targetPrUrl} --idempotency-key ${idempotencyKey}`,
|
|
3449
|
+
requestId: err instanceof CliError ? err.requestId : void 0
|
|
3450
|
+
}
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3453
|
+
if (options.wait) {
|
|
3454
|
+
const waitPollIntervalMs = parsePollInterval(options.pollInterval);
|
|
3455
|
+
if (!isJson(command, options)) {
|
|
3456
|
+
console.log(`Session: ${sessionId}`);
|
|
3457
|
+
if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
|
|
3458
|
+
}
|
|
3459
|
+
await waitForCreatedPrompt(sessionId, promptId, sessionData.sessionUrl, waitPollIntervalMs, runtime, command);
|
|
3460
|
+
if (!isJson(command, options)) {
|
|
3461
|
+
console.log(`Target PR: ${targetPrUrl}`);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
if (isJson(command, options)) {
|
|
3465
|
+
const output = { sessionId, repoUrl, targetPrUrl };
|
|
3466
|
+
if (sessionData.sessionUrl) output.sessionUrl = sessionData.sessionUrl;
|
|
3467
|
+
if (options.model) output.model = options.model;
|
|
3468
|
+
if (options.backend) output.agentRuntimeBackend = agentRuntimeBackend;
|
|
3469
|
+
if (options.reasoningEffort) output.reasoningEffort = options.reasoningEffort;
|
|
3470
|
+
if (promptId) output.promptId = promptId;
|
|
3471
|
+
writeJson(output);
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
if (options.wait) return;
|
|
3475
|
+
console.log(`Session: ${sessionId}`);
|
|
3476
|
+
if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
|
|
3477
|
+
console.log(`Target PR: ${targetPrUrl}`);
|
|
3478
|
+
console.log(`Follow with: arcanist sessions events ${sessionId} --follow --json`);
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3266
3481
|
// src/commands/sandbox.ts
|
|
3267
3482
|
import { execFileSync } from "child_process";
|
|
3268
3483
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -4682,6 +4897,25 @@ JSON mode returns {sessionId, promptId?}
|
|
|
4682
4897
|
`
|
|
4683
4898
|
);
|
|
4684
4899
|
}
|
|
4900
|
+
function addQaOptions(cmd) {
|
|
4901
|
+
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(
|
|
4902
|
+
"--poll-interval <ms>",
|
|
4903
|
+
"Polling interval in milliseconds while waiting",
|
|
4904
|
+
String(DEFAULT_WATCH_POLL_INTERVAL_MS)
|
|
4905
|
+
).option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText(
|
|
4906
|
+
"after",
|
|
4907
|
+
`
|
|
4908
|
+
Examples:
|
|
4909
|
+
arcanist sessions qa https://github.com/org/repo/pull/123
|
|
4910
|
+
arcanist sessions qa https://github.com/org/repo/pull/123 --model gpt-5.4 --wait
|
|
4911
|
+
arcanist sessions qa https://github.com/org/repo/pull/123 --idempotency-key 1f0e6f1a-...
|
|
4912
|
+
|
|
4913
|
+
JSON:
|
|
4914
|
+
JSON mode returns {sessionId, sessionUrl?, repoUrl, targetPrUrl, model?, agentRuntimeBackend?, reasoningEffort?, promptId?}
|
|
4915
|
+
Inspect progress with the session URL or the session events stream.
|
|
4916
|
+
`
|
|
4917
|
+
);
|
|
4918
|
+
}
|
|
4685
4919
|
var auth = program.command("auth").description("Authentication commands");
|
|
4686
4920
|
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(
|
|
4687
4921
|
"after",
|
|
@@ -4707,6 +4941,9 @@ addCreateOptions(sessions.command("create").description("Create a session and se
|
|
|
4707
4941
|
addSendOptions(sessions.command("send").description("Send a message to an existing session")).action(
|
|
4708
4942
|
(sessionId, prompt, options, command) => messageCommand(sessionId, prompt, options, command)
|
|
4709
4943
|
);
|
|
4944
|
+
addQaOptions(sessions.command("qa").description("Start a QA verification session for a GitHub pull request")).action(
|
|
4945
|
+
(prUrl, options, command) => qaCommand(prUrl, options, command)
|
|
4946
|
+
);
|
|
4710
4947
|
sessions.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").addHelpText(
|
|
4711
4948
|
"after",
|
|
4712
4949
|
`
|