@tryarcanist/cli 0.1.166 → 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.
Files changed (3) hide show
  1. package/README.md +34 -1
  2. package/dist/index.js +179 -20
  3. 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. The CLI derives separate session and prompt idempotency keys from the provided value.
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
@@ -2655,6 +2655,31 @@ function renderWatchEvent(event, state) {
2655
2655
  }
2656
2656
  }
2657
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
+
2658
2683
  // ../../shared/session/transient-disconnect.ts
2659
2684
  var TRANSIENT_DISCONNECT_VISIBILITY_MS = 15e3;
2660
2685
  function nextDisconnectMask(mask, previous, current, now) {
@@ -2930,26 +2955,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
2930
2955
  const runtime = getRuntimeOptions(command, options);
2931
2956
  assertArcanistSessionMutationAllowed("create");
2932
2957
  const config = requireConfig(runtime);
2933
- let agentRuntimeBackend = CODEX_AGENT_RUNTIME_BACKEND;
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
- }
2958
+ const agentRuntimeBackend = resolveModelAndBackend(options);
2953
2959
  const prompt = options.onboarding ? ONBOARDING_PROMPT : await resolvePromptInput(promptArg, options);
2954
2960
  const waitPollIntervalMs = options.wait ? parsePollInterval(options.pollInterval) : null;
2955
2961
  const repoError = validateRepoUrl(repoUrl);
@@ -3263,6 +3269,137 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
3263
3269
  console.log(`Message sent to session ${sessionId}.`);
3264
3270
  }
3265
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
+
3266
3403
  // src/commands/sandbox.ts
3267
3404
  import { execFileSync } from "child_process";
3268
3405
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
@@ -4682,6 +4819,25 @@ JSON mode returns {sessionId, promptId?}
4682
4819
  `
4683
4820
  );
4684
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
+ }
4685
4841
  var auth = program.command("auth").description("Authentication commands");
4686
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(
4687
4843
  "after",
@@ -4707,6 +4863,9 @@ addCreateOptions(sessions.command("create").description("Create a session and se
4707
4863
  addSendOptions(sessions.command("send").description("Send a message to an existing session")).action(
4708
4864
  (sessionId, prompt, options, command) => messageCommand(sessionId, prompt, options, command)
4709
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
+ );
4710
4869
  sessions.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").addHelpText(
4711
4870
  "after",
4712
4871
  `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.166",
3
+ "version": "0.1.167",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {