@whittlelabs/sifter 0.30.23 → 0.32.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.
Files changed (3) hide show
  1. package/bin.js +2315 -783
  2. package/bin.js.map +4 -4
  3. package/package.json +1 -1
package/bin.js CHANGED
@@ -9876,7 +9876,7 @@ var require_progress = __commonJS({
9876
9876
  "../../packages/shuttle/dist/executors/progress.js"(exports2) {
9877
9877
  "use strict";
9878
9878
  Object.defineProperty(exports2, "__esModule", { value: true });
9879
- exports2.StreamJsonTap = exports2.ProgressReporter = void 0;
9879
+ exports2.JsonLineTap = exports2.StreamJsonTap = exports2.ProgressReporter = void 0;
9880
9880
  var ProgressReporter = class {
9881
9881
  sink;
9882
9882
  seq = 0;
@@ -10017,112 +10017,1300 @@ var require_progress = __commonJS({
10017
10017
  }
10018
10018
  return void 0;
10019
10019
  }
10020
+ var JsonLineTap = class {
10021
+ onEvent;
10022
+ buf = "";
10023
+ constructor(onEvent) {
10024
+ this.onEvent = onEvent;
10025
+ }
10026
+ push(chunk) {
10027
+ this.buf += chunk;
10028
+ let nl;
10029
+ while ((nl = this.buf.indexOf("\n")) !== -1) {
10030
+ const line = this.buf.slice(0, nl);
10031
+ this.buf = this.buf.slice(nl + 1);
10032
+ this.consume(line);
10033
+ }
10034
+ }
10035
+ end() {
10036
+ if (this.buf.trim().length > 0)
10037
+ this.consume(this.buf);
10038
+ this.buf = "";
10039
+ }
10040
+ consume(line) {
10041
+ const trimmed = line.trim();
10042
+ if (!trimmed.startsWith("{"))
10043
+ return;
10044
+ let event;
10045
+ try {
10046
+ event = JSON.parse(trimmed);
10047
+ } catch {
10048
+ return;
10049
+ }
10050
+ try {
10051
+ this.onEvent(event);
10052
+ } catch {
10053
+ }
10054
+ }
10055
+ };
10056
+ exports2.JsonLineTap = JsonLineTap;
10057
+ }
10058
+ });
10059
+
10060
+ // ../../packages/shuttle/dist/executors/agent-process.js
10061
+ var require_agent_process = __commonJS({
10062
+ "../../packages/shuttle/dist/executors/agent-process.js"(exports2) {
10063
+ "use strict";
10064
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
10065
+ return mod && mod.__esModule ? mod : { "default": mod };
10066
+ };
10067
+ Object.defineProperty(exports2, "__esModule", { value: true });
10068
+ exports2.STDERR_LIMIT = void 0;
10069
+ exports2.runAgentProcess = runAgentProcess;
10070
+ exports2.firstLine = firstLine;
10071
+ exports2.keepTranscript = keepTranscript;
10072
+ exports2.classifyAgentFault = classifyAgentFault;
10073
+ exports2.agentFailure = agentFailure;
10074
+ var types_1 = require_types2();
10075
+ var child_process_1 = require("child_process");
10076
+ var fs_1 = require("fs");
10077
+ var path_1 = __importDefault(require("path"));
10078
+ var string_decoder_1 = require("string_decoder");
10079
+ exports2.STDERR_LIMIT = 4e3;
10080
+ function runAgentProcess(opts) {
10081
+ return new Promise((resolve, reject) => {
10082
+ let child;
10083
+ try {
10084
+ child = (0, child_process_1.spawn)(opts.bin, opts.args, {
10085
+ cwd: opts.cwd,
10086
+ stdio: ["pipe", "pipe", "pipe"],
10087
+ ...opts.env ? { env: opts.env } : {}
10088
+ });
10089
+ } catch (err) {
10090
+ const e = err;
10091
+ resolve({ code: null, stdout: "", stderr: "", spawnError: { code: e.code ?? null, message: e.message } });
10092
+ return;
10093
+ }
10094
+ const stdoutChunks = [];
10095
+ let stderr = "";
10096
+ let settled = false;
10097
+ let timer;
10098
+ const decoder = opts.onStdout ? new string_decoder_1.StringDecoder("utf8") : null;
10099
+ const finish = (fn) => {
10100
+ if (settled)
10101
+ return;
10102
+ settled = true;
10103
+ if (timer)
10104
+ clearTimeout(timer);
10105
+ opts.signal.removeEventListener("abort", onAbort);
10106
+ fn();
10107
+ };
10108
+ function onAbort() {
10109
+ child.kill("SIGTERM");
10110
+ finish(() => reject(new Error("Dispatch aborted")));
10111
+ }
10112
+ if (opts.signal.aborted) {
10113
+ child.kill("SIGTERM");
10114
+ finish(() => reject(new Error("Dispatch aborted")));
10115
+ return;
10116
+ }
10117
+ opts.signal.addEventListener("abort", onAbort, { once: true });
10118
+ const armIdleTimer = () => {
10119
+ if (settled)
10120
+ return;
10121
+ if (timer)
10122
+ clearTimeout(timer);
10123
+ timer = setTimeout(() => {
10124
+ child.kill("SIGTERM");
10125
+ finish(() => reject(new Error(`${opts.toolName} produced no output for ${opts.idleTimeoutMs}ms; killed as hung`)));
10126
+ }, opts.idleTimeoutMs);
10127
+ };
10128
+ armIdleTimer();
10129
+ child.stdout?.on("data", (chunk) => {
10130
+ stdoutChunks.push(chunk);
10131
+ armIdleTimer();
10132
+ if (decoder && opts.onStdout)
10133
+ opts.onStdout(decoder.write(chunk));
10134
+ });
10135
+ child.stderr?.on("data", (chunk) => {
10136
+ if (stderr.length < exports2.STDERR_LIMIT)
10137
+ stderr += chunk.toString("utf-8");
10138
+ });
10139
+ child.stdin?.on("error", () => {
10140
+ });
10141
+ if (opts.stdin !== void 0)
10142
+ child.stdin?.write(opts.stdin);
10143
+ child.stdin?.end();
10144
+ child.on("error", (err) => {
10145
+ const code = err.code ?? null;
10146
+ finish(() => resolve({
10147
+ code: null,
10148
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
10149
+ stderr,
10150
+ spawnError: { code, message: err.message }
10151
+ }));
10152
+ });
10153
+ child.on("close", (code) => {
10154
+ if (decoder && opts.onStdout)
10155
+ opts.onStdout(decoder.end());
10156
+ finish(() => resolve({ code, stdout: Buffer.concat(stdoutChunks).toString("utf-8"), stderr }));
10157
+ });
10158
+ });
10159
+ }
10160
+ function firstLine(text) {
10161
+ for (const line of text.split("\n")) {
10162
+ const trimmed = line.trim();
10163
+ if (trimmed.length > 0)
10164
+ return trimmed.length > 300 ? `${trimmed.slice(0, 300)}...` : trimmed;
10165
+ }
10166
+ return null;
10167
+ }
10168
+ async function keepTranscript(dir, name, stdout) {
10169
+ try {
10170
+ await fs_1.promises.mkdir(dir, { recursive: true });
10171
+ const safe = name.replace(/[^A-Za-z0-9._-]/g, "_");
10172
+ await fs_1.promises.writeFile(path_1.default.join(dir, `${safe}.jsonl`), stdout);
10173
+ } catch (err) {
10174
+ console.warn(`[shuttle] could not keep the transcript for ${name} under ${dir}: ${err instanceof Error ? err.message : String(err)}`);
10175
+ }
10176
+ }
10177
+ function classifyAgentFault(failure, vocab) {
10178
+ if (failure.spawnErrorCode === "ENOENT")
10179
+ return vocab.installRemedy;
10180
+ const text = failure.text ?? "";
10181
+ if (failure.apiErrorStatus === 401 || vocab.signedOut.test(text))
10182
+ return vocab.loginRemedy;
10183
+ if (vocab.notEntitled?.pattern.test(text))
10184
+ return vocab.notEntitled.remedy;
10185
+ if (failure.apiErrorStatus === 402 || failure.apiErrorStatus === 429 || vocab.outOfUsage.test(text)) {
10186
+ return vocab.limitRemedy;
10187
+ }
10188
+ return null;
10189
+ }
10190
+ function agentFailure(local, message, facts, vocab) {
10191
+ if (!local)
10192
+ return new Error(message);
10193
+ const remedy = classifyAgentFault(facts, vocab);
10194
+ return remedy ? new types_1.ExecutorEnvironmentError(message, remedy) : new Error(message);
10195
+ }
10196
+ }
10197
+ });
10198
+
10199
+ // ../../packages/shuttle/dist/executors/json-answer.js
10200
+ var require_json_answer = __commonJS({
10201
+ "../../packages/shuttle/dist/executors/json-answer.js"(exports2) {
10202
+ "use strict";
10203
+ Object.defineProperty(exports2, "__esModule", { value: true });
10204
+ exports2.extractJsonObject = extractJsonObject;
10205
+ function extractJsonObject(s) {
10206
+ const exact = tryParseObject(s);
10207
+ if (exact)
10208
+ return exact;
10209
+ let last;
10210
+ let i = 0;
10211
+ while (i < s.length) {
10212
+ if (s[i] !== "{") {
10213
+ i++;
10214
+ continue;
10215
+ }
10216
+ let depth = 0;
10217
+ let inString = false;
10218
+ let escaped = false;
10219
+ let end = -1;
10220
+ for (let j = i; j < s.length; j++) {
10221
+ const ch = s[j];
10222
+ if (escaped) {
10223
+ escaped = false;
10224
+ continue;
10225
+ }
10226
+ if (ch === "\\") {
10227
+ escaped = inString;
10228
+ continue;
10229
+ }
10230
+ if (ch === '"') {
10231
+ inString = !inString;
10232
+ continue;
10233
+ }
10234
+ if (inString)
10235
+ continue;
10236
+ if (ch === "{")
10237
+ depth++;
10238
+ else if (ch === "}") {
10239
+ depth--;
10240
+ if (depth === 0) {
10241
+ end = j;
10242
+ break;
10243
+ }
10244
+ }
10245
+ }
10246
+ if (end === -1)
10247
+ break;
10248
+ const candidate = tryParseObject(s.slice(i, end + 1));
10249
+ if (candidate)
10250
+ last = candidate;
10251
+ i = end + 1;
10252
+ }
10253
+ return last;
10254
+ }
10255
+ function tryParseObject(s) {
10256
+ try {
10257
+ const parsed = JSON.parse(s);
10258
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
10259
+ } catch {
10260
+ return void 0;
10261
+ }
10262
+ }
10263
+ }
10264
+ });
10265
+
10266
+ // ../../packages/shuttle/dist/executors/models.js
10267
+ var require_models = __commonJS({
10268
+ "../../packages/shuttle/dist/executors/models.js"(exports2) {
10269
+ "use strict";
10270
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
10271
+ return mod && mod.__esModule ? mod : { "default": mod };
10272
+ };
10273
+ Object.defineProperty(exports2, "__esModule", { value: true });
10274
+ exports2.LIST_MODELS_TIMEOUT_MS = exports2.CLAUDE_CODE_EFFORTS = exports2.REASONING_EFFORTS = void 0;
10275
+ exports2.parseReasoningEffort = parseReasoningEffort;
10276
+ exports2.hintedEffort = hintedEffort;
10277
+ exports2.runListing = runListing;
10278
+ var os_1 = __importDefault(require("os"));
10279
+ var agent_process_1 = require_agent_process();
10280
+ exports2.REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
10281
+ exports2.CLAUDE_CODE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
10282
+ function parseReasoningEffort(value, levels) {
10283
+ if (value === void 0)
10284
+ return void 0;
10285
+ if (!levels.includes(value)) {
10286
+ throw new Error(`"reasoningEffort" must be one of ${levels.join(", ")}`);
10287
+ }
10288
+ return value;
10289
+ }
10290
+ function hintedEffort(hints) {
10291
+ const effort = hints.effort;
10292
+ return typeof effort === "string" && /^[a-z]+$/.test(effort) ? effort : void 0;
10293
+ }
10294
+ exports2.LIST_MODELS_TIMEOUT_MS = 15e3;
10295
+ async function runListing(bin, args, signal, env) {
10296
+ try {
10297
+ const run = await (0, agent_process_1.runAgentProcess)({
10298
+ bin,
10299
+ args,
10300
+ cwd: os_1.default.tmpdir(),
10301
+ ...env ? { env } : {},
10302
+ idleTimeoutMs: exports2.LIST_MODELS_TIMEOUT_MS,
10303
+ toolName: bin,
10304
+ signal
10305
+ });
10306
+ return run.spawnError || run.code !== 0 ? null : run.stdout;
10307
+ } catch {
10308
+ return null;
10309
+ }
10310
+ }
10311
+ }
10312
+ });
10313
+
10314
+ // ../../packages/shuttle/dist/executors/workspace-telemetry.js
10315
+ var require_workspace_telemetry = __commonJS({
10316
+ "../../packages/shuttle/dist/executors/workspace-telemetry.js"(exports2) {
10317
+ "use strict";
10318
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
10319
+ return mod && mod.__esModule ? mod : { "default": mod };
10320
+ };
10321
+ Object.defineProperty(exports2, "__esModule", { value: true });
10322
+ exports2.workspaceOutputs = workspaceOutputs;
10323
+ exports2.relativeWorkspaceReads = relativeWorkspaceReads;
10324
+ exports2.globToRegExp = globToRegExp;
10325
+ exports2.creditOverlayReads = creditOverlayReads;
10326
+ exports2.shellReadPaths = shellReadPaths;
10327
+ var fs_1 = require("fs");
10328
+ var path_1 = __importDefault(require("path"));
10329
+ function workspaceOutputs(workspace, runCwd, readPaths, toolInputs) {
10330
+ const reads = [...readPaths];
10331
+ for (const rel of creditOverlayReads(toolInputs, workspace.overlay?.writtenPaths ?? [], runCwd)) {
10332
+ reads.push(path_1.default.join(runCwd, rel));
10333
+ }
10334
+ return [
10335
+ { label: "workspace_reads", content: { paths: relativeWorkspaceReads(reads, runCwd) } },
10336
+ {
10337
+ // Structured audit trail: reviewers and operators can see whether a
10338
+ // given review actually explored the checkout or fell back to the
10339
+ // diff (and why, including a failed private-repo auth), plus which
10340
+ // overlay files landed.
10341
+ label: "workspace",
10342
+ content: {
10343
+ materialized: workspace.materialized,
10344
+ ...workspace.warning ? { warning: workspace.warning } : {},
10345
+ ...workspace.context.length > 0 ? {
10346
+ context: workspace.context.map((c) => ({
10347
+ name: c.requestedName,
10348
+ materialized: c.materialized,
10349
+ ...c.warning ? { warning: c.warning } : {}
10350
+ }))
10351
+ } : {},
10352
+ ...workspace.overlay ? {
10353
+ overlay: {
10354
+ written: workspace.overlay.written,
10355
+ ...workspace.overlay.skipped.length > 0 ? { skipped: workspace.overlay.skipped } : {}
10356
+ }
10357
+ } : {}
10358
+ }
10359
+ }
10360
+ ];
10361
+ }
10362
+ var MAX_REPORTED_READS = 2e3;
10363
+ function workspaceRoots(cwd) {
10364
+ const root = path_1.default.resolve(cwd);
10365
+ const roots = [root];
10366
+ try {
10367
+ const real = (0, fs_1.realpathSync)(root);
10368
+ if (real !== root)
10369
+ roots.push(real);
10370
+ } catch {
10371
+ }
10372
+ return roots;
10373
+ }
10374
+ function relativeWorkspaceReads(readPaths, cwd) {
10375
+ const roots = workspaceRoots(cwd);
10376
+ const root = roots[0];
10377
+ const out = /* @__PURE__ */ new Set();
10378
+ for (const p of readPaths) {
10379
+ const resolved = path_1.default.resolve(root, p);
10380
+ for (const r of roots) {
10381
+ if (resolved === r)
10382
+ break;
10383
+ if (!resolved.startsWith(r + path_1.default.sep))
10384
+ continue;
10385
+ out.add(resolved.slice(r.length + 1));
10386
+ break;
10387
+ }
10388
+ if (out.size >= MAX_REPORTED_READS)
10389
+ break;
10390
+ }
10391
+ return [...out].sort();
10392
+ }
10393
+ var GLOB_CHARS = /[*?[{]/;
10394
+ function escapeRegExp(s) {
10395
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10396
+ }
10397
+ function globToRegExp(glob) {
10398
+ let re = "^";
10399
+ for (let i = 0; i < glob.length; i++) {
10400
+ const ch = glob[i];
10401
+ if (ch === "*") {
10402
+ if (glob[i + 1] === "*") {
10403
+ if (glob[i + 2] === "/") {
10404
+ re += "(?:.*/)?";
10405
+ i += 2;
10406
+ } else {
10407
+ re += ".*";
10408
+ i += 1;
10409
+ }
10410
+ } else {
10411
+ re += "[^/]*";
10412
+ }
10413
+ } else if (ch === "?") {
10414
+ re += "[^/]";
10415
+ } else if (ch === "[") {
10416
+ const end = glob.indexOf("]", i + 1);
10417
+ if (end === -1)
10418
+ return null;
10419
+ let cls = glob.slice(i + 1, end);
10420
+ if (cls.startsWith("!"))
10421
+ cls = `^${cls.slice(1)}`;
10422
+ re += `[${cls.replace(/\\/g, "\\\\")}]`;
10423
+ i = end;
10424
+ } else if (ch === "{") {
10425
+ const end = glob.indexOf("}", i + 1);
10426
+ if (end === -1)
10427
+ return null;
10428
+ re += `(?:${glob.slice(i + 1, end).split(",").map(escapeRegExp).join("|")})`;
10429
+ i = end;
10430
+ } else {
10431
+ re += escapeRegExp(ch);
10432
+ }
10433
+ }
10434
+ return new RegExp(`${re}$`);
10435
+ }
10436
+ function creditOverlayReads(toolInputs, writtenPaths, runCwd) {
10437
+ if (writtenPaths.length === 0 || toolInputs.length === 0)
10438
+ return [];
10439
+ const roots = runCwd ? workspaceRoots(runCwd) : [];
10440
+ const dirs = /* @__PURE__ */ new Set();
10441
+ for (const rel of writtenPaths) {
10442
+ const parts = rel.split("/");
10443
+ for (let i = 1; i < parts.length; i++)
10444
+ dirs.add(`${parts.slice(0, i).join("/")}/`);
10445
+ }
10446
+ const dirAlt = [...dirs].map(escapeRegExp).join("|");
10447
+ const wordRe = dirs.size > 0 ? new RegExp(`(?:${dirAlt})[^\\s"'\`;|&<>()\\\\]*`, "g") : null;
10448
+ const findRe = dirs.size > 0 ? new RegExp(`\\bfind\\s+(?:\\./)?(${[...dirs].map((d) => escapeRegExp(d.slice(0, -1))).join("|")})/?(?=\\s|$)`, "g") : null;
10449
+ const readerRe = new RegExp(`(?:^|[\\s"])(?:-exec|-execdir|xargs)\\b[^;|&]*\\b${READERS}\\b`);
10450
+ const credited = /* @__PURE__ */ new Set();
10451
+ for (const raw of toolInputs) {
10452
+ const input = roots.reduce((s, r) => s.split(`${r}/`).join(""), raw);
10453
+ for (const rel of writtenPaths) {
10454
+ if (input.includes(rel))
10455
+ credited.add(rel);
10456
+ }
10457
+ if (wordRe) {
10458
+ for (const match of input.matchAll(wordRe)) {
10459
+ const word = match[0];
10460
+ if (!GLOB_CHARS.test(word))
10461
+ continue;
10462
+ const re = globToRegExp(word);
10463
+ if (!re)
10464
+ continue;
10465
+ for (const rel of writtenPaths) {
10466
+ if (re.test(rel))
10467
+ credited.add(rel);
10468
+ }
10469
+ }
10470
+ }
10471
+ if (findRe && readerRe.test(input)) {
10472
+ for (const match of input.matchAll(findRe)) {
10473
+ const dir = `${match[1]}/`;
10474
+ for (const rel of writtenPaths) {
10475
+ if (rel.startsWith(dir))
10476
+ credited.add(rel);
10477
+ }
10478
+ }
10479
+ }
10480
+ if (!PLAIN_READER_RE.test(input))
10481
+ continue;
10482
+ for (const match of input.matchAll(CD_RE)) {
10483
+ const target = match[1].replace(/^\.\//, "").replace(/\/+$/, "");
10484
+ const base = target.length > 0 ? `${target}/` : "";
10485
+ for (const rel of writtenPaths) {
10486
+ if (!rel.startsWith(base))
10487
+ continue;
10488
+ if (namesBareWord(input, rel.slice(base.length)))
10489
+ credited.add(rel);
10490
+ }
10491
+ }
10492
+ }
10493
+ return [...credited].sort();
10494
+ }
10495
+ var READERS = "(?:cat|head|tail|sed|awk|less|more|bat)";
10496
+ var PLAIN_READER_RE = new RegExp(`(?:^|[\\s"'\`;|&(])${READERS}\\b`);
10497
+ var CD_RE = /(?:^|[\s"'`;|&(])cd\s+(?:--\s+)?["']?([^\s"'`;|&<>()\\]+)/g;
10498
+ function namesBareWord(input, word) {
10499
+ const edge = String.raw`[\s"'\`;|&<>(),\\]`;
10500
+ return new RegExp(`(?:^|${edge})${escapeRegExp(word)}(?=$|${edge})`).test(input);
10501
+ }
10502
+ function shellReadPaths(commands, runCwd) {
10503
+ const roots = workspaceRoots(runCwd);
10504
+ const out = /* @__PURE__ */ new Set();
10505
+ for (const command of commands) {
10506
+ if (!PLAIN_READER_RE.test(command))
10507
+ continue;
10508
+ for (const raw of command.split(/[\s"'`;|&<>()]+/)) {
10509
+ const word = raw.replace(/^\.\//, "");
10510
+ if (word.length === 0 || word.startsWith("-") || GLOB_CHARS.test(word))
10511
+ continue;
10512
+ const abs = path_1.default.resolve(roots[0], roots.reduce((w, r) => w.startsWith(`${r}/`) ? w.slice(r.length + 1) : w, word));
10513
+ if (!roots.some((r) => abs.startsWith(r + path_1.default.sep)))
10514
+ continue;
10515
+ try {
10516
+ if ((0, fs_1.statSync)(abs).isFile())
10517
+ out.add(abs);
10518
+ } catch {
10519
+ }
10520
+ if (out.size >= MAX_REPORTED_READS)
10521
+ return [...out];
10522
+ }
10523
+ }
10524
+ return [...out];
10525
+ }
10526
+ }
10527
+ });
10528
+
10529
+ // ../../packages/shuttle/dist/executors/claude-code.js
10530
+ var require_claude_code = __commonJS({
10531
+ "../../packages/shuttle/dist/executors/claude-code.js"(exports2) {
10532
+ "use strict";
10533
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
10534
+ return mod && mod.__esModule ? mod : { "default": mod };
10535
+ };
10536
+ Object.defineProperty(exports2, "__esModule", { value: true });
10537
+ exports2.classifyEnvironmentFault = classifyEnvironmentFault;
10538
+ exports2.createClaudeCodeExecutor = createClaudeCodeExecutor;
10539
+ exports2.checkoutScopedTools = checkoutScopedTools;
10540
+ exports2.parseStreamJson = parseStreamJson;
10541
+ var fs_1 = require("fs");
10542
+ var os_1 = __importDefault(require("os"));
10543
+ var path_1 = __importDefault(require("path"));
10544
+ var loom_1 = require_dist();
10545
+ var types_1 = require_types2();
10546
+ var validate_output_1 = require_validate_output();
10547
+ var progress_1 = require_progress();
10548
+ var agent_process_1 = require_agent_process();
10549
+ var json_answer_1 = require_json_answer();
10550
+ var models_1 = require_models();
10551
+ var workspace_telemetry_1 = require_workspace_telemetry();
10552
+ var apply_1 = require_apply();
10553
+ var DEFAULT_TIMEOUT = 6e5;
10554
+ var REVIEW_TOOLS = ["Read", "Grep", "Glob"];
10555
+ function reviewRunArgs(settingSources) {
10556
+ return [
10557
+ "--setting-sources",
10558
+ settingSources,
10559
+ "--strict-mcp-config",
10560
+ "--tools",
10561
+ REVIEW_TOOLS.join(","),
10562
+ "--permission-mode",
10563
+ "default",
10564
+ "--settings",
10565
+ JSON.stringify({ disableAllHooks: true })
10566
+ ];
10567
+ }
10568
+ var RETIRED_READ_TOOLS = ["LS"];
10569
+ var HOSTED_ENV_PASSTHROUGH = [
10570
+ "PATH",
10571
+ "HOME",
10572
+ "TMPDIR",
10573
+ "LANG",
10574
+ "LC_ALL",
10575
+ "LC_CTYPE",
10576
+ "TZ",
10577
+ "NODE_EXTRA_CA_CERTS",
10578
+ "SSL_CERT_FILE",
10579
+ "SSL_CERT_DIR",
10580
+ "HTTPS_PROXY",
10581
+ "HTTP_PROXY",
10582
+ "NO_PROXY",
10583
+ "https_proxy",
10584
+ "http_proxy",
10585
+ "no_proxy"
10586
+ ];
10587
+ function describeClaudeFailure(meta, fallback) {
10588
+ if (!meta)
10589
+ return fallback;
10590
+ const status = meta.apiErrorStatus !== null ? ` (provider HTTP ${meta.apiErrorStatus})` : "";
10591
+ if (meta.detail)
10592
+ return `${meta.detail}${status}`;
10593
+ const subtype = meta.subtype && meta.subtype !== "success" ? `: ${meta.subtype}` : "";
10594
+ return `${fallback}${subtype}${status}`;
10595
+ }
10596
+ var CLAUDE_CODE_MODELS = [
10597
+ { id: "opus", label: "Claude Opus (latest)", efforts: [...models_1.CLAUDE_CODE_EFFORTS] },
10598
+ { id: "fable", label: "Claude Fable (latest)", efforts: [...models_1.CLAUDE_CODE_EFFORTS] },
10599
+ { id: "sonnet", label: "Claude Sonnet (latest)", efforts: [...models_1.CLAUDE_CODE_EFFORTS] },
10600
+ { id: "haiku", label: "Claude Haiku (latest)", efforts: [...models_1.CLAUDE_CODE_EFFORTS] }
10601
+ ];
10602
+ var READINESS_PROMPT = "Reply with the single word: ok";
10603
+ var READINESS_TIMEOUT_MS = 12e4;
10604
+ var INSTALL_REMEDY = "install Claude Code (npm install -g @anthropic-ai/claude-code) where this process can find it on PATH";
10605
+ var LOGIN_REMEDY = "run `claude` once and sign in (or set a valid ANTHROPIC_API_KEY); if your login or provider is configured in Claude Code settings (apiKeyHelper, or an env block), set `claudeSettings: user` on the claude-code executor, since a review loads no settings file without it";
10606
+ var LIMIT_REMEDY = "wait for the plan's usage limit to reset, or raise it";
10607
+ var SIGNED_OUT = /invalid api key|please run \/login|not (?:logged|signed) in|oauth token (?:has )?expired|authentication_error|invalid x-api-key/i;
10608
+ var OUT_OF_USAGE = /usage limit|credit balance is too low|out of (?:extra )?usage|hit your (?:\w+ )?(?:usage |spend )?limit|5-hour limit/i;
10609
+ var CLAUDE_FAULTS = {
10610
+ installRemedy: INSTALL_REMEDY,
10611
+ loginRemedy: LOGIN_REMEDY,
10612
+ limitRemedy: LIMIT_REMEDY,
10613
+ signedOut: SIGNED_OUT,
10614
+ outOfUsage: OUT_OF_USAGE
10615
+ };
10616
+ function classifyEnvironmentFault(failure) {
10617
+ return (0, agent_process_1.classifyAgentFault)(failure, CLAUDE_FAULTS);
10618
+ }
10619
+ function runFailure(local, message, facts) {
10620
+ return (0, agent_process_1.agentFailure)(local, message, facts, CLAUDE_FAULTS);
10621
+ }
10622
+ var ClaudeCodeExecutor = class {
10623
+ capability;
10624
+ strategies = [loom_1.PROMPT_EXECUTION_STRATEGY];
10625
+ config;
10626
+ constructor(config) {
10627
+ this.config = config;
10628
+ this.capability = { id: config.capabilityId };
10629
+ }
10630
+ estimate(dispatch) {
10631
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
10632
+ return {
10633
+ inputTokens: spec.costCapHint?.estimatedInputTokens,
10634
+ outputTokens: spec.costCapHint?.estimatedOutputTokens
10635
+ };
10636
+ }
10637
+ /**
10638
+ * Declare the run environment to the runner (ADR 0026): the opaque workspace
10639
+ * bag plus the materialization config (tmp dir, overlay allowlist). No
10640
+ * credentials here (ADR 0027) — the runner's broker reveals them on demand:
10641
+ * this executor pulls its model key from `ctx.credentials` in `execute()`,
10642
+ * and the git request references its own fetch credential. The executor
10643
+ * touches neither git nor Keep.
10644
+ */
10645
+ async prepareEnvironment(dispatch) {
10646
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
10647
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : void 0;
10648
+ return {
10649
+ ...spec.workspace ? { workspaceRequest: spec.workspace } : {},
10650
+ materializeOptions: {
10651
+ ...tmpBase ? { tmpDir: tmpBase } : {},
10652
+ overlayAllowedHosts: this.config.overlayAllowedHosts ?? []
10653
+ }
10654
+ };
10655
+ }
10656
+ async execute(dispatch, signal, ctx) {
10657
+ const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
10658
+ const workspace = ctx?.workspace ?? null;
10659
+ let modelKey = null;
10660
+ if (ctx?.credentials) {
10661
+ const hints = (0, loom_1.pickProviderHints)(spec, "anthropic-api");
10662
+ const customerId = typeof hints.customerId === "string" ? hints.customerId : void 0;
10663
+ if (!customerId) {
10664
+ throw new Error("claude-code dynamic-key mode requires providerHints.anthropicApi.customerId on every job");
10665
+ }
10666
+ modelKey = await ctx.credentials.reveal({ name: "managed-model-key", principal: customerId });
10667
+ }
10668
+ let jobScratch = null;
10669
+ let configDir;
10670
+ if (modelKey) {
10671
+ jobScratch = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-cc-"));
10672
+ configDir = path_1.default.join(jobScratch, "claude-config");
10673
+ await fs_1.promises.mkdir(configDir);
10674
+ }
10675
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
10676
+ let promptOnlyScratch = null;
10677
+ const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
10678
+ try {
10679
+ const childEnv = buildChildEnv(modelKey, configDir);
10680
+ const claudeHints = (0, loom_1.pickProviderHints)(spec, "claude-code");
10681
+ const hintedModel = typeof claudeHints.model === "string" && claudeHints.model.length > 0 ? claudeHints.model : void 0;
10682
+ const effort = (0, models_1.hintedEffort)(claudeHints);
10683
+ if (!workspace) {
10684
+ promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
10685
+ }
10686
+ const runCwd = workspace?.cwd ?? promptOnlyScratch;
10687
+ const { response, readPaths, toolInputs } = await this.runClaude(
10688
+ prompt,
10689
+ spec,
10690
+ runCwd,
10691
+ childEnv,
10692
+ signal,
10693
+ hintedModel,
10694
+ reporter,
10695
+ void 0,
10696
+ dispatch.job.id,
10697
+ // Local mode is the operator's own login: a failure of THAT is the
10698
+ // machine's, and the runner pauses on it. With a revealed key it is one
10699
+ // customer's, and only their job fails.
10700
+ modelKey === null,
10701
+ effort
10702
+ );
10703
+ if (workspace) {
10704
+ response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, readPaths, toolInputs));
10705
+ }
10706
+ return response;
10707
+ } finally {
10708
+ reporter?.close();
10709
+ if (promptOnlyScratch)
10710
+ await fs_1.promises.rm(promptOnlyScratch, { recursive: true, force: true }).catch(() => {
10711
+ });
10712
+ if (jobScratch)
10713
+ await fs_1.promises.rm(jobScratch, { recursive: true, force: true }).catch(() => {
10714
+ });
10715
+ }
10716
+ }
10717
+ /**
10718
+ * In-session gate repair (ADR 0017): resume the session `execute()` returned a
10719
+ * handle for and correct its output. The repair turn works from the resumed
10720
+ * session's own context (no checkout needed), under the same `--json-schema`
10721
+ * constraint. A throwaway cwd hosts the run; the same handle rides back out so
10722
+ * a second repair round can resume again.
10723
+ */
10724
+ async continue(continuation, followUp, signal, _ctx) {
10725
+ const handle = continuation;
10726
+ if (!handle || typeof handle.sessionId !== "string") {
10727
+ throw new Error("claude-code continue: missing session to resume");
10728
+ }
10729
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
10730
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-resume-"));
10731
+ try {
10732
+ const spec = {
10733
+ rendering: "producer",
10734
+ outputLabel: handle.outputLabel,
10735
+ outputSchema: handle.outputSchema
10736
+ };
10737
+ const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId, `${handle.sessionId}-repair`, handle.childEnv === void 0, handle.effort);
10738
+ return response;
10739
+ } finally {
10740
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
10741
+ });
10742
+ }
10743
+ }
10744
+ /**
10745
+ * Can this machine run a job now? One real one-turn call through the CLI,
10746
+ * the same probe `doctor --deep` makes. The runner asks it only while
10747
+ * claiming is paused on an `ExecutorEnvironmentError`, and only in local
10748
+ * mode. A signed-out or missing CLI fails before any model call, so the
10749
+ * re-check spends nothing until the login works again, and then one turn.
10750
+ */
10751
+ async checkReadiness(signal) {
10752
+ let run;
10753
+ try {
10754
+ run = await (0, agent_process_1.runAgentProcess)({
10755
+ bin: "claude",
10756
+ args: ["--print", "--output-format", "stream-json", "--verbose", "--max-turns", "1", READINESS_PROMPT],
10757
+ cwd: process.cwd(),
10758
+ idleTimeoutMs: READINESS_TIMEOUT_MS,
10759
+ toolName: "the claude CLI",
10760
+ signal
10761
+ });
10762
+ } catch (err) {
10763
+ const message = err instanceof Error ? err.message : String(err);
10764
+ return { ready: false, reason: message === "Dispatch aborted" ? "readiness check aborted" : message };
10765
+ }
10766
+ if (run.spawnError) {
10767
+ return {
10768
+ ready: false,
10769
+ reason: run.spawnError.code === "ENOENT" ? `the claude CLI is not on this machine's PATH (${run.spawnError.message})` : run.spawnError.message
10770
+ };
10771
+ }
10772
+ let meta = null;
10773
+ try {
10774
+ meta = parseStreamJson(run.stdout).resultMeta;
10775
+ } catch {
10776
+ meta = null;
10777
+ }
10778
+ if (run.code === 0 && !meta?.isError)
10779
+ return { ready: true };
10780
+ const said = meta?.detail ?? (0, agent_process_1.firstLine)(run.stderr) ?? (0, agent_process_1.firstLine)(run.stdout);
10781
+ return { ready: false, reason: said ?? `the claude CLI exited with code ${run.code}` };
10782
+ }
10783
+ /** The CLI's aliases (see CLAUDE_CODE_MODELS); nothing to ask, so nothing can fail. */
10784
+ async listModels() {
10785
+ return CLAUDE_CODE_MODELS;
10786
+ }
10787
+ async runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName, local = false, effortOverride) {
10788
+ const outputLabel = spec.outputLabel;
10789
+ const model = modelOverride ?? this.config.model;
10790
+ const effort = effortOverride ?? this.config.reasoningEffort;
10791
+ const allowedTools = local ? this.config.allowedTools : checkoutScopedTools(this.config.allowedTools);
10792
+ if (!local && this.config.claudeSettings === "user") {
10793
+ throw new Error("claude-code: `claudeSettings: user` is for an installed Sifter whose login lives in Claude Code settings; a hosted run authenticates with the key it is handed and loads no settings");
10794
+ }
10795
+ const settingSources = local && this.config.claudeSettings === "user" ? "user" : "";
10796
+ const args = ["--print", "--output-format", "stream-json", "--verbose", ...reviewRunArgs(settingSources)];
10797
+ if (resumeSessionId)
10798
+ args.push("--resume", resumeSessionId);
10799
+ if (spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0) {
10800
+ args.push("--json-schema", JSON.stringify(spec.outputSchema));
10801
+ }
10802
+ if (model)
10803
+ args.push("--model", model);
10804
+ if (effort)
10805
+ args.push("--effort", effort);
10806
+ if (this.config.maxTurns !== void 0)
10807
+ args.push("--max-turns", String(this.config.maxTurns));
10808
+ if (allowedTools && allowedTools.length > 0) {
10809
+ args.push("--allowedTools", allowedTools.join(","));
10810
+ }
10811
+ const tap = reporter ? new progress_1.StreamJsonTap({
10812
+ onReasoning: (t) => reporter.reasoning(t),
10813
+ onTool: (name, target) => reporter.tool(name, target),
10814
+ onResult: () => reporter.flush()
10815
+ }) : null;
10816
+ const run = await (0, agent_process_1.runAgentProcess)({
10817
+ bin: "claude",
10818
+ args,
10819
+ cwd,
10820
+ // Undefined in local mode → inherit the ambient env (and its
10821
+ // `~/.claude` login) unchanged.
10822
+ env: childEnv,
10823
+ stdin: prompt,
10824
+ idleTimeoutMs: this.config.timeout ?? DEFAULT_TIMEOUT,
10825
+ toolName: "Claude Code",
10826
+ signal,
10827
+ ...tap ? { onStdout: (text) => tap.push(text) } : {}
10828
+ });
10829
+ tap?.end();
10830
+ if (run.spawnError) {
10831
+ if (local && run.spawnError.code === "ENOENT") {
10832
+ throw new types_1.ExecutorEnvironmentError(`the claude CLI is not on this machine's PATH (${run.spawnError.message})`, INSTALL_REMEDY);
10833
+ }
10834
+ throw new Error(run.spawnError.message);
10835
+ }
10836
+ const { stdout, stderr: stderrText, code } = run;
10837
+ if (this.config.keepTranscripts && transcriptName) {
10838
+ void (0, agent_process_1.keepTranscript)((0, apply_1.expandHome)(this.config.keepTranscripts), transcriptName, stdout);
10839
+ }
10840
+ if (code !== 0) {
10841
+ let meta = null;
10842
+ try {
10843
+ meta = parseStreamJson(stdout).resultMeta;
10844
+ } catch {
10845
+ }
10846
+ const said = meta?.detail ?? (0, agent_process_1.firstLine)(stderrText) ?? (0, agent_process_1.firstLine)(stdout);
10847
+ const fallback = `Claude Code exited with code ${code}`;
10848
+ const failure = runFailure(local, describeClaudeFailure(meta, fallback), {
10849
+ apiErrorStatus: meta?.apiErrorStatus ?? null,
10850
+ text: said
10851
+ });
10852
+ if (failure instanceof types_1.ExecutorEnvironmentError && !meta && said) {
10853
+ throw new types_1.ExecutorEnvironmentError(`${fallback}: ${said}`, failure.remedy);
10854
+ }
10855
+ throw failure;
10856
+ }
10857
+ const stream = parseStreamJson(stdout);
10858
+ if (stream.resultMeta?.isError) {
10859
+ throw runFailure(local, describeClaudeFailure(stream.resultMeta, "Claude Code reported a failure"), {
10860
+ apiErrorStatus: stream.resultMeta.apiErrorStatus,
10861
+ text: stream.resultMeta.detail ?? (0, agent_process_1.firstLine)(stderrText)
10862
+ });
10863
+ }
10864
+ const answerText = stream.finalText ?? stdout;
10865
+ const parsed = stream.structuredOutput ?? (0, json_answer_1.extractJsonObject)(answerText);
10866
+ if (parsed === void 0 && stream.resultMeta) {
10867
+ throw new Error(`claude's final message contained no JSON object (result subtype=${stream.resultMeta.subtype ?? "unknown"}; message head: ${JSON.stringify(answerText.slice(0, 160))})`);
10868
+ }
10869
+ (0, validate_output_1.assertOutputValid)(spec, parsed);
10870
+ return {
10871
+ response: {
10872
+ outputs: [{ label: outputLabel, content: parsed ?? { raw: answerText } }],
10873
+ rawText: answerText,
10874
+ parsed,
10875
+ usage: {
10876
+ aiProvider: "claude-code",
10877
+ // The model that ran (an alias resolved), over the one asked for.
10878
+ ...stream.model ?? model ? { aiModel: stream.model ?? model } : {},
10879
+ ...effort ? { aiEffort: effort } : {},
10880
+ ...stream.usage ?? {}
10881
+ },
10882
+ // In-session repair handle (ADR 0017): present only when the run
10883
+ // reported a session id, so the runner can `--resume` it.
10884
+ ...stream.sessionId ? {
10885
+ continuation: {
10886
+ sessionId: stream.sessionId,
10887
+ model,
10888
+ effort,
10889
+ childEnv,
10890
+ outputLabel,
10891
+ outputSchema: spec.outputSchema
10892
+ }
10893
+ } : {}
10894
+ },
10895
+ readPaths: stream.readPaths,
10896
+ toolInputs: stream.toolInputs
10897
+ };
10898
+ }
10899
+ };
10900
+ function createClaudeCodeExecutor(config, capabilityId = "claude-code") {
10901
+ const validated = { capabilityId };
10902
+ if (config.tmpDir !== void 0) {
10903
+ if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
10904
+ throw new Error('"tmpDir" must be a non-empty string');
10905
+ }
10906
+ validated.tmpDir = config.tmpDir;
10907
+ }
10908
+ if (config.model !== void 0) {
10909
+ if (typeof config.model !== "string")
10910
+ throw new Error('"model" must be a string');
10911
+ validated.model = config.model;
10912
+ }
10913
+ const effort = (0, models_1.parseReasoningEffort)(config.reasoningEffort, models_1.CLAUDE_CODE_EFFORTS);
10914
+ if (effort)
10915
+ validated.reasoningEffort = effort;
10916
+ if (config.maxTurns !== void 0) {
10917
+ if (typeof config.maxTurns !== "number" || !Number.isInteger(config.maxTurns) || config.maxTurns < 1) {
10918
+ throw new Error('"maxTurns" must be a positive integer');
10919
+ }
10920
+ validated.maxTurns = config.maxTurns;
10921
+ }
10922
+ if (config.allowedTools !== void 0) {
10923
+ if (!Array.isArray(config.allowedTools) || !config.allowedTools.every((t) => typeof t === "string")) {
10924
+ throw new Error('"allowedTools" must be an array of strings');
10925
+ }
10926
+ validated.allowedTools = config.allowedTools;
10927
+ }
10928
+ if (config.timeout !== void 0) {
10929
+ if (typeof config.timeout !== "number" || config.timeout <= 0) {
10930
+ throw new Error('"timeout" must be a positive number');
10931
+ }
10932
+ validated.timeout = config.timeout;
10933
+ }
10934
+ if (config.keepTranscripts !== void 0) {
10935
+ if (typeof config.keepTranscripts !== "string" || config.keepTranscripts.length === 0) {
10936
+ throw new Error('"keepTranscripts" must be a non-empty directory path');
10937
+ }
10938
+ validated.keepTranscripts = config.keepTranscripts;
10939
+ }
10940
+ if (config.claudeSettings !== void 0) {
10941
+ if (config.claudeSettings !== "user") {
10942
+ throw new Error('"claudeSettings" may only be "user" (leave it unset to load no settings file)');
10943
+ }
10944
+ validated.claudeSettings = "user";
10945
+ }
10946
+ if (config.overlayAllowedHosts !== void 0) {
10947
+ if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
10948
+ throw new Error('"overlayAllowedHosts" must be an array of strings');
10949
+ }
10950
+ validated.overlayAllowedHosts = config.overlayAllowedHosts;
10951
+ }
10952
+ return new ClaudeCodeExecutor(validated);
10953
+ }
10954
+ function checkoutScopedTools(tools) {
10955
+ if (!tools)
10956
+ return void 0;
10957
+ const scoped = [];
10958
+ for (const tool of tools) {
10959
+ if (RETIRED_READ_TOOLS.some((t) => tool === t || tool === `${t}(./**)`))
10960
+ continue;
10961
+ const name = REVIEW_TOOLS.find((t) => tool === t || tool === `${t}(./**)`);
10962
+ if (!name) {
10963
+ throw new Error(`claude-code: a hosted run may use only ${REVIEW_TOOLS.join(", ")} inside the checkout; "${tool}" is not allowed`);
10964
+ }
10965
+ scoped.push(`${name}(./**)`);
10966
+ }
10967
+ return scoped;
10968
+ }
10969
+ function buildChildEnv(modelKey, configDir) {
10970
+ if (!modelKey && !configDir)
10971
+ return void 0;
10972
+ const env = {};
10973
+ for (const name of HOSTED_ENV_PASSTHROUGH) {
10974
+ if (process.env[name] !== void 0)
10975
+ env[name] = process.env[name];
10976
+ }
10977
+ return {
10978
+ ...env,
10979
+ ...modelKey ? { ANTHROPIC_API_KEY: modelKey } : {},
10980
+ ...configDir ? { CLAUDE_CONFIG_DIR: configDir } : {}
10981
+ };
10982
+ }
10983
+ function parseStreamJson(stdout) {
10984
+ let finalText = null;
10985
+ const readPaths = [];
10986
+ let usage = null;
10987
+ let resultMeta = null;
10988
+ let structuredOutput = null;
10989
+ let sessionId = null;
10990
+ let model = null;
10991
+ const toolInputs = [];
10992
+ for (const line of stdout.split("\n")) {
10993
+ const trimmed = line.trim();
10994
+ if (!trimmed.startsWith("{"))
10995
+ continue;
10996
+ let event;
10997
+ try {
10998
+ event = JSON.parse(trimmed);
10999
+ } catch {
11000
+ continue;
11001
+ }
11002
+ if (typeof event.session_id === "string")
11003
+ sessionId = event.session_id;
11004
+ if (event.type === "system" && event.subtype === "init" && typeof event.model === "string" && event.model) {
11005
+ model = event.model;
11006
+ } else if (event.type === "assistant") {
11007
+ const content = event.message?.content;
11008
+ if (!Array.isArray(content))
11009
+ continue;
11010
+ for (const block of content) {
11011
+ const b = block;
11012
+ if (b?.type !== "tool_use")
11013
+ continue;
11014
+ const fp = b.input?.file_path;
11015
+ if (b.name === "Read" && typeof fp === "string") {
11016
+ readPaths.push(fp);
11017
+ }
11018
+ if (b.input && typeof b.input === "object")
11019
+ toolInputs.push(JSON.stringify(b.input));
11020
+ }
11021
+ } else if (event.type === "result") {
11022
+ if (typeof event.result === "string" && event.result.trim().length > 0) {
11023
+ finalText = event.result;
11024
+ }
11025
+ resultMeta = {
11026
+ subtype: typeof event.subtype === "string" ? event.subtype : null,
11027
+ isError: event.is_error === true,
11028
+ // On a failure the CLI puts a human-readable reason in `result` — the
11029
+ // model was unavailable, spend limit hit, and so on. It is the only
11030
+ // place that reason exists, so keep it for the failure message.
11031
+ detail: typeof event.result === "string" && event.result.trim().length > 0 ? event.result.trim() : null,
11032
+ apiErrorStatus: typeof event.api_error_status === "number" ? event.api_error_status : null
11033
+ };
11034
+ const so = event.structured_output;
11035
+ if (so && typeof so === "object" && !Array.isArray(so)) {
11036
+ structuredOutput = so;
11037
+ }
11038
+ const u = event.usage;
11039
+ const cost = event.total_cost_usd;
11040
+ usage = {
11041
+ ...typeof u?.input_tokens === "number" ? { inputTokens: u.input_tokens } : {},
11042
+ ...typeof u?.output_tokens === "number" ? { outputTokens: u.output_tokens } : {},
11043
+ ...typeof cost === "number" ? { costUsd: cost } : {}
11044
+ };
11045
+ }
11046
+ }
11047
+ return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, model, toolInputs };
11048
+ }
10020
11049
  }
10021
11050
  });
10022
11051
 
10023
- // ../../packages/shuttle/dist/executors/claude-code.js
10024
- var require_claude_code = __commonJS({
10025
- "../../packages/shuttle/dist/executors/claude-code.js"(exports2) {
11052
+ // ../../packages/shuttle/dist/executors/agent-common.js
11053
+ var require_agent_common = __commonJS({
11054
+ "../../packages/shuttle/dist/executors/agent-common.js"(exports2) {
11055
+ "use strict";
11056
+ Object.defineProperty(exports2, "__esModule", { value: true });
11057
+ exports2.DEFAULT_AGENT_TIMEOUT = void 0;
11058
+ exports2.parseAgentCommonConfig = parseAgentCommonConfig;
11059
+ exports2.agentEnvironment = agentEnvironment;
11060
+ exports2.refuseHosted = refuseHosted;
11061
+ var loom_1 = require_dist();
11062
+ var apply_1 = require_apply();
11063
+ exports2.DEFAULT_AGENT_TIMEOUT = 6e5;
11064
+ function parseAgentCommonConfig(config, capabilityId) {
11065
+ const out = { capabilityId };
11066
+ if (config.tmpDir !== void 0) {
11067
+ if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
11068
+ throw new Error('"tmpDir" must be a non-empty string');
11069
+ }
11070
+ out.tmpDir = config.tmpDir;
11071
+ }
11072
+ if (config.model !== void 0) {
11073
+ if (typeof config.model !== "string" || config.model.length === 0) {
11074
+ throw new Error('"model" must be a non-empty string');
11075
+ }
11076
+ out.model = config.model;
11077
+ }
11078
+ if (config.timeout !== void 0) {
11079
+ if (typeof config.timeout !== "number" || config.timeout <= 0) {
11080
+ throw new Error('"timeout" must be a positive number');
11081
+ }
11082
+ out.timeout = config.timeout;
11083
+ }
11084
+ if (config.keepTranscripts !== void 0) {
11085
+ if (typeof config.keepTranscripts !== "string" || config.keepTranscripts.length === 0) {
11086
+ throw new Error('"keepTranscripts" must be a non-empty directory path');
11087
+ }
11088
+ out.keepTranscripts = config.keepTranscripts;
11089
+ }
11090
+ if (config.overlayAllowedHosts !== void 0) {
11091
+ if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
11092
+ throw new Error('"overlayAllowedHosts" must be an array of strings');
11093
+ }
11094
+ out.overlayAllowedHosts = config.overlayAllowedHosts;
11095
+ }
11096
+ return out;
11097
+ }
11098
+ function agentEnvironment(dispatch, config) {
11099
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
11100
+ const tmpBase = config.tmpDir ? (0, apply_1.expandHome)(config.tmpDir) : void 0;
11101
+ return {
11102
+ ...spec.workspace ? { workspaceRequest: spec.workspace } : {},
11103
+ materializeOptions: {
11104
+ ...tmpBase ? { tmpDir: tmpBase } : {},
11105
+ overlayAllowedHosts: config.overlayAllowedHosts ?? []
11106
+ }
11107
+ };
11108
+ }
11109
+ function refuseHosted(executor, hosted) {
11110
+ if (hosted) {
11111
+ throw new Error(`${executor}: this executor runs on an installed Sifter only, on the operator's own login; a hosted Sifter has no managed credential for it`);
11112
+ }
11113
+ }
11114
+ }
11115
+ });
11116
+
11117
+ // ../../packages/shuttle/dist/executors/strict-schema.js
11118
+ var require_strict_schema = __commonJS({
11119
+ "../../packages/shuttle/dist/executors/strict-schema.js"(exports2) {
11120
+ "use strict";
11121
+ Object.defineProperty(exports2, "__esModule", { value: true });
11122
+ exports2.toStrict = toStrict;
11123
+ exports2.dropNulls = dropNulls;
11124
+ var UNSUPPORTED = ["format", "default", "$schema"];
11125
+ function toStrict(schema) {
11126
+ if (Array.isArray(schema))
11127
+ return schema.map(toStrict);
11128
+ if (!isSchema(schema))
11129
+ return schema;
11130
+ const out = {};
11131
+ for (const [key, value] of Object.entries(schema)) {
11132
+ if (UNSUPPORTED.includes(key))
11133
+ continue;
11134
+ if (key === "properties" && isSchema(value)) {
11135
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
11136
+ const props = {};
11137
+ for (const [name, prop] of Object.entries(value)) {
11138
+ const strict = toStrict(prop);
11139
+ props[name] = required.has(name) ? strict : nullable(strict);
11140
+ }
11141
+ out.properties = props;
11142
+ out.required = Object.keys(value);
11143
+ continue;
11144
+ }
11145
+ if (key === "required")
11146
+ continue;
11147
+ out[key] = SUBSCHEMA_KEYS.has(key) ? toStrict(value) : value;
11148
+ }
11149
+ if (out.type === "object" || out.properties !== void 0)
11150
+ out.additionalProperties = false;
11151
+ return out;
11152
+ }
11153
+ function dropNulls(value, schema) {
11154
+ if (!isSchema(schema))
11155
+ return value;
11156
+ if (Array.isArray(value)) {
11157
+ return isSchema(schema.items) ? value.map((v) => dropNulls(v, schema.items)) : value;
11158
+ }
11159
+ if (!isSchema(value)) {
11160
+ return value;
11161
+ }
11162
+ const branches = [schema.anyOf, schema.oneOf].find(Array.isArray);
11163
+ if (branches) {
11164
+ const branch = branches.find((b) => isSchema(b) && (b.type === "object" || isSchema(b.properties)));
11165
+ return branch ? dropNulls(value, branch) : value;
11166
+ }
11167
+ if (!isSchema(schema.properties))
11168
+ return value;
11169
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
11170
+ const out = {};
11171
+ for (const [name, v] of Object.entries(value)) {
11172
+ const prop = schema.properties[name];
11173
+ if (v === null && !required.has(name) && !allowsNull(prop))
11174
+ continue;
11175
+ out[name] = dropNulls(v, prop);
11176
+ }
11177
+ return out;
11178
+ }
11179
+ var SUBSCHEMA_KEYS = /* @__PURE__ */ new Set(["items", "anyOf", "oneOf", "allOf", "not", "additionalProperties", "definitions", "$defs"]);
11180
+ function nullable(schema) {
11181
+ if (!isSchema(schema) || allowsNull(schema))
11182
+ return schema;
11183
+ if (typeof schema.type === "string" && schema.anyOf === void 0 && schema.oneOf === void 0) {
11184
+ const out = { ...schema, type: [schema.type, "null"] };
11185
+ if (Array.isArray(schema.enum))
11186
+ out.enum = [...schema.enum, null];
11187
+ return out;
11188
+ }
11189
+ return { anyOf: [schema, { type: "null" }] };
11190
+ }
11191
+ function allowsNull(schema) {
11192
+ if (!isSchema(schema))
11193
+ return false;
11194
+ if (schema.type === "null")
11195
+ return true;
11196
+ if (Array.isArray(schema.type) && schema.type.includes("null"))
11197
+ return true;
11198
+ const branches = [schema.anyOf, schema.oneOf].find(Array.isArray);
11199
+ return branches?.some(allowsNull) ?? false;
11200
+ }
11201
+ function isSchema(v) {
11202
+ return typeof v === "object" && v !== null && !Array.isArray(v);
11203
+ }
11204
+ }
11205
+ });
11206
+
11207
+ // ../../packages/shuttle/dist/executors/codex.js
11208
+ var require_codex = __commonJS({
11209
+ "../../packages/shuttle/dist/executors/codex.js"(exports2) {
10026
11210
  "use strict";
10027
11211
  var __importDefault = exports2 && exports2.__importDefault || function(mod) {
10028
11212
  return mod && mod.__esModule ? mod : { "default": mod };
10029
11213
  };
10030
11214
  Object.defineProperty(exports2, "__esModule", { value: true });
10031
- exports2.classifyEnvironmentFault = classifyEnvironmentFault;
10032
- exports2.createClaudeCodeExecutor = createClaudeCodeExecutor;
10033
- exports2.checkoutScopedTools = checkoutScopedTools;
10034
- exports2.parseStreamJson = parseStreamJson;
10035
- exports2.relativeWorkspaceReads = relativeWorkspaceReads;
10036
- exports2.globToRegExp = globToRegExp;
10037
- exports2.creditOverlayReads = creditOverlayReads;
10038
- exports2.extractJsonObject = extractJsonObject;
10039
- var child_process_1 = require("child_process");
11215
+ exports2.resolveCodex = resolveCodex;
11216
+ exports2.codexReviewArgs = codexReviewArgs;
11217
+ exports2.parseCodexEvents = parseCodexEvents;
11218
+ exports2.parseCodexModels = parseCodexModels;
11219
+ exports2.createCodexExecutor = createCodexExecutor;
10040
11220
  var fs_1 = require("fs");
10041
11221
  var os_1 = __importDefault(require("os"));
10042
11222
  var path_1 = __importDefault(require("path"));
10043
- var string_decoder_1 = require("string_decoder");
10044
11223
  var loom_1 = require_dist();
10045
11224
  var types_1 = require_types2();
10046
11225
  var validate_output_1 = require_validate_output();
10047
11226
  var progress_1 = require_progress();
11227
+ var agent_process_1 = require_agent_process();
11228
+ var agent_common_1 = require_agent_common();
11229
+ var json_answer_1 = require_json_answer();
11230
+ var models_1 = require_models();
11231
+ var strict_schema_1 = require_strict_schema();
11232
+ var workspace_telemetry_1 = require_workspace_telemetry();
10048
11233
  var apply_1 = require_apply();
10049
- var DEFAULT_TIMEOUT = 6e5;
10050
- var REVIEW_TOOLS = ["Read", "Grep", "Glob"];
10051
- function reviewRunArgs(settingSources) {
10052
- return [
10053
- "--setting-sources",
10054
- settingSources,
10055
- "--strict-mcp-config",
10056
- "--tools",
10057
- REVIEW_TOOLS.join(","),
10058
- "--permission-mode",
10059
- "default",
10060
- "--settings",
10061
- JSON.stringify({ disableAllHooks: true })
10062
- ];
11234
+ var CODEX_PROFILE = "sift-review";
11235
+ function reviewProfile(installDir, platform = process.platform) {
11236
+ const rules = ['":root"="deny"', '":minimal"="read"', '":workspace_roots"="read"', `${JSON.stringify(installDir)}="read"`];
11237
+ if (platform === "linux") {
11238
+ rules.push(`${JSON.stringify(path_1.default.join(codexHome(), "tmp", "arg0"))}="read"`);
11239
+ } else {
11240
+ rules.push('":tmpdir"="deny"', '":slash_tmp"="deny"');
11241
+ }
11242
+ return `permissions.${CODEX_PROFILE}={extends=":read-only", filesystem={${rules.join(", ")}}}`;
10063
11243
  }
10064
- var RETIRED_READ_TOOLS = ["LS"];
10065
- var HOSTED_ENV_PASSTHROUGH = [
10066
- "PATH",
10067
- "HOME",
10068
- "TMPDIR",
10069
- "LANG",
10070
- "LC_ALL",
10071
- "LC_CTYPE",
10072
- "TZ",
10073
- "NODE_EXTRA_CA_CERTS",
10074
- "SSL_CERT_FILE",
10075
- "SSL_CERT_DIR",
10076
- "HTTPS_PROXY",
10077
- "HTTP_PROXY",
10078
- "NO_PROXY",
10079
- "https_proxy",
10080
- "http_proxy",
10081
- "no_proxy"
10082
- ];
10083
- function describeClaudeFailure(meta, fallback) {
10084
- if (!meta)
10085
- return fallback;
10086
- const status = meta.apiErrorStatus !== null ? ` (provider HTTP ${meta.apiErrorStatus})` : "";
10087
- if (meta.detail)
10088
- return `${meta.detail}${status}`;
10089
- const subtype = meta.subtype && meta.subtype !== "success" ? `: ${meta.subtype}` : "";
10090
- return `${fallback}${subtype}${status}`;
11244
+ function codexHome() {
11245
+ return process.env.CODEX_HOME ?? path_1.default.join(os_1.default.homedir(), ".codex");
10091
11246
  }
10092
- var READINESS_PROMPT = "Reply with the single word: ok";
10093
- var READINESS_TIMEOUT_MS = 12e4;
10094
- var STDERR_LIMIT = 4e3;
10095
- var INSTALL_REMEDY = "install Claude Code (npm install -g @anthropic-ai/claude-code) where this process can find it on PATH";
10096
- var LOGIN_REMEDY = "run `claude` once and sign in (or set a valid ANTHROPIC_API_KEY); if your login or provider is configured in Claude Code settings (apiKeyHelper, or an env block), set `claudeSettings: user` on the claude-code executor, since a review loads no settings file without it";
10097
- var LIMIT_REMEDY = "wait for the plan's usage limit to reset, or raise it";
10098
- var SIGNED_OUT = /invalid api key|please run \/login|not (?:logged|signed) in|oauth token (?:has )?expired|authentication_error|invalid x-api-key/i;
10099
- var OUT_OF_USAGE = /usage limit|credit balance is too low|out of (?:extra )?usage|hit your (?:\w+ )?(?:usage |spend )?limit|5-hour limit/i;
10100
- function classifyEnvironmentFault(failure) {
10101
- if (failure.spawnErrorCode === "ENOENT")
10102
- return INSTALL_REMEDY;
10103
- const text = failure.text ?? "";
10104
- if (failure.apiErrorStatus === 401 || SIGNED_OUT.test(text))
10105
- return LOGIN_REMEDY;
10106
- if (failure.apiErrorStatus === 402 || failure.apiErrorStatus === 429 || OUT_OF_USAGE.test(text)) {
10107
- return LIMIT_REMEDY;
11247
+ function resolveCodex(pathEnv = process.env.PATH ?? "") {
11248
+ for (const dir of pathEnv.split(path_1.default.delimiter)) {
11249
+ if (!dir)
11250
+ continue;
11251
+ const candidate = path_1.default.join(dir, "codex");
11252
+ let real;
11253
+ try {
11254
+ real = (0, fs_1.realpathSync)(candidate);
11255
+ if (!(0, fs_1.statSync)(real).isFile())
11256
+ continue;
11257
+ } catch {
11258
+ continue;
11259
+ }
11260
+ const installDir = real.endsWith(".js") ? path_1.default.dirname(path_1.default.dirname(real)) : path_1.default.dirname(real);
11261
+ return { bin: real.endsWith(".js") ? candidate : real, installDir };
10108
11262
  }
10109
11263
  return null;
10110
11264
  }
10111
- function firstLine(text) {
10112
- for (const line of text.split("\n")) {
10113
- const trimmed = line.trim();
10114
- if (trimmed.length > 0)
10115
- return trimmed.length > 300 ? `${trimmed.slice(0, 300)}...` : trimmed;
11265
+ var DISABLED_FEATURES = [
11266
+ "hooks",
11267
+ "apps",
11268
+ "plugins",
11269
+ "remote_plugin",
11270
+ "plugin_sharing",
11271
+ "skill_mcp_dependency_install",
11272
+ "browser_use",
11273
+ "browser_use_external",
11274
+ "browser_use_full_cdp_access",
11275
+ "browser_annotation_api",
11276
+ "computer_use",
11277
+ "image_generation",
11278
+ "workspace_dependencies"
11279
+ ];
11280
+ function requireCodex() {
11281
+ const codex = resolveCodex();
11282
+ if (!codex) {
11283
+ throw new types_1.ExecutorEnvironmentError("the codex CLI is not on this machine's PATH", CODEX_FAULTS.installRemedy);
10116
11284
  }
10117
- return null;
11285
+ return codex;
10118
11286
  }
10119
- function runFailure(local, message, facts) {
10120
- if (!local)
10121
- return new Error(message);
10122
- const remedy = classifyEnvironmentFault(facts);
10123
- return remedy ? new types_1.ExecutorEnvironmentError(message, remedy) : new Error(message);
11287
+ function codexReviewArgs(installDir, platform = process.platform) {
11288
+ return [
11289
+ "--json",
11290
+ "--skip-git-repo-check",
11291
+ "--ignore-user-config",
11292
+ "--ignore-rules",
11293
+ "-c",
11294
+ reviewProfile(installDir, platform),
11295
+ "-c",
11296
+ `default_permissions="${CODEX_PROFILE}"`,
11297
+ "-c",
11298
+ 'approval_policy="never"',
11299
+ "-c",
11300
+ 'web_search="disabled"',
11301
+ ...DISABLED_FEATURES.flatMap((f) => ["--disable", f])
11302
+ ];
10124
11303
  }
10125
- var ClaudeCodeExecutor = class {
11304
+ var CODEX_FAULTS = {
11305
+ installRemedy: "install Codex (brew install codex, or npm install -g @openai/codex) where this process can find it on PATH",
11306
+ loginRemedy: "run `codex login` and sign in (or `codex login --with-api-key`)",
11307
+ limitRemedy: "wait for the plan's usage limit to reset, or raise it",
11308
+ signedOut: /not logged in|run `?codex login|401 unauthorized|invalid api key|refresh token|token (?:has )?expired|sign in again/i,
11309
+ outOfUsage: /usage limit|hit your (?:\w+ )?limit|insufficient_quota|exceeded your current quota|429 too many requests/i
11310
+ };
11311
+ var READINESS_PROMPT = "Reply with the single word: ok";
11312
+ var READINESS_TIMEOUT_MS = 12e4;
11313
+ var CodexExecutor = class {
10126
11314
  capability;
10127
11315
  strategies = [loom_1.PROMPT_EXECUTION_STRATEGY];
10128
11316
  config;
@@ -10137,132 +11325,81 @@ var require_claude_code = __commonJS({
10137
11325
  outputTokens: spec.costCapHint?.estimatedOutputTokens
10138
11326
  };
10139
11327
  }
10140
- /**
10141
- * Declare the run environment to the runner (ADR 0026): the opaque workspace
10142
- * bag plus the materialization config (tmp dir, overlay allowlist). No
10143
- * credentials here (ADR 0027) — the runner's broker reveals them on demand:
10144
- * this executor pulls its model key from `ctx.credentials` in `execute()`,
10145
- * and the git request references its own fetch credential. The executor
10146
- * touches neither git nor Keep.
10147
- */
10148
11328
  async prepareEnvironment(dispatch) {
10149
- const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
10150
- const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : void 0;
10151
- return {
10152
- ...spec.workspace ? { workspaceRequest: spec.workspace } : {},
10153
- materializeOptions: {
10154
- ...tmpBase ? { tmpDir: tmpBase } : {},
10155
- overlayAllowedHosts: this.config.overlayAllowedHosts ?? []
10156
- }
10157
- };
11329
+ return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
10158
11330
  }
10159
11331
  async execute(dispatch, signal, ctx) {
11332
+ (0, agent_common_1.refuseHosted)("codex", Boolean(ctx?.credentials));
10160
11333
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
10161
11334
  const workspace = ctx?.workspace ?? null;
10162
- let modelKey = null;
10163
- if (ctx?.credentials) {
10164
- const hints = (0, loom_1.pickProviderHints)(spec, "anthropic-api");
10165
- const customerId = typeof hints.customerId === "string" ? hints.customerId : void 0;
10166
- if (!customerId) {
10167
- throw new Error("claude-code dynamic-key mode requires providerHints.anthropicApi.customerId on every job");
10168
- }
10169
- modelKey = await ctx.credentials.reveal({ name: "managed-model-key", principal: customerId });
10170
- }
10171
- let jobScratch = null;
10172
- let configDir;
10173
- if (modelKey) {
10174
- jobScratch = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-cc-"));
10175
- configDir = path_1.default.join(jobScratch, "claude-config");
10176
- await fs_1.promises.mkdir(configDir);
10177
- }
10178
- const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
10179
- let promptOnlyScratch = null;
11335
+ const hints = (0, loom_1.pickProviderHints)(spec, "codex");
11336
+ const model = typeof hints.model === "string" && hints.model.length > 0 ? hints.model : this.config.model;
11337
+ const effort = (0, models_1.hintedEffort)(hints) ?? this.config.reasoningEffort;
10180
11338
  const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
11339
+ const codex = requireCodex();
11340
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-codex-"));
10181
11341
  try {
10182
- const childEnv = buildChildEnv(modelKey, configDir);
10183
- const claudeHints = (0, loom_1.pickProviderHints)(spec, "claude-code");
10184
- const hintedModel = typeof claudeHints.model === "string" && claudeHints.model.length > 0 ? claudeHints.model : void 0;
10185
- if (!workspace) {
10186
- promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
10187
- }
10188
- const runCwd = workspace?.cwd ?? promptOnlyScratch;
10189
- const { response, readPaths, toolInputs } = await this.runClaude(
11342
+ let runCwd = workspace?.cwd;
11343
+ if (!runCwd) {
11344
+ runCwd = path_1.default.join(scratch, "run");
11345
+ await fs_1.promises.mkdir(runCwd);
11346
+ }
11347
+ const { response, commands } = await this.runCodex({
11348
+ codex,
11349
+ args: ["exec", ...codexReviewArgs(codex.installDir), "-C", runCwd],
10190
11350
  prompt,
10191
11351
  spec,
10192
- runCwd,
10193
- childEnv,
11352
+ cwd: runCwd,
11353
+ scratch,
11354
+ model,
11355
+ effort,
10194
11356
  signal,
10195
- hintedModel,
10196
11357
  reporter,
10197
- void 0,
10198
- dispatch.job.id,
10199
- // Local mode is the operator's own login: a failure of THAT is the
10200
- // machine's, and the runner pauses on it. With a revealed key it is one
10201
- // customer's, and only their job fails.
10202
- modelKey === null
10203
- );
11358
+ transcriptName: dispatch.job.id
11359
+ });
10204
11360
  if (workspace) {
10205
- const reads = [...readPaths];
10206
- for (const rel of creditOverlayReads(toolInputs, workspace.overlay?.writtenPaths ?? [], runCwd)) {
10207
- reads.push(path_1.default.join(runCwd, rel));
10208
- }
10209
- response.outputs.push({
10210
- label: "workspace_reads",
10211
- content: { paths: relativeWorkspaceReads(reads, runCwd) }
10212
- });
10213
- response.outputs.push({
10214
- label: "workspace",
10215
- content: {
10216
- materialized: workspace.materialized,
10217
- ...workspace.warning ? { warning: workspace.warning } : {},
10218
- ...workspace.context.length > 0 ? {
10219
- context: workspace.context.map((c) => ({
10220
- name: c.requestedName,
10221
- materialized: c.materialized,
10222
- ...c.warning ? { warning: c.warning } : {}
10223
- }))
10224
- } : {},
10225
- ...workspace.overlay ? {
10226
- overlay: {
10227
- written: workspace.overlay.written,
10228
- ...workspace.overlay.skipped.length > 0 ? { skipped: workspace.overlay.skipped } : {}
10229
- }
10230
- } : {}
10231
- }
10232
- });
11361
+ response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, (0, workspace_telemetry_1.shellReadPaths)(commands, runCwd), commands.map((command) => JSON.stringify({ command }))));
10233
11362
  }
10234
11363
  return response;
10235
11364
  } finally {
10236
11365
  reporter?.close();
10237
- if (promptOnlyScratch)
10238
- await fs_1.promises.rm(promptOnlyScratch, { recursive: true, force: true }).catch(() => {
10239
- });
10240
- if (jobScratch)
10241
- await fs_1.promises.rm(jobScratch, { recursive: true, force: true }).catch(() => {
10242
- });
11366
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
11367
+ });
10243
11368
  }
10244
11369
  }
10245
11370
  /**
10246
- * In-session gate repair (ADR 0017): resume the session `execute()` returned a
10247
- * handle for and correct its output. The repair turn works from the resumed
10248
- * session's own context (no checkout needed), under the same `--json-schema`
10249
- * constraint. A throwaway cwd hosts the run; the same handle rides back out so
10250
- * a second repair round can resume again.
11371
+ * In-session gate repair (ADR 0017): resume the thread and send the
11372
+ * follow-up under the same schema. The checkout is gone by now (the runner
11373
+ * cleans up before gates run), so the repair works from the thread's own
11374
+ * context in a scratch dir, as claude-code's does.
10251
11375
  */
10252
- async continue(continuation, followUp, signal, _ctx) {
11376
+ async continue(continuation, followUp, signal, ctx) {
11377
+ (0, agent_common_1.refuseHosted)("codex", Boolean(ctx?.credentials));
10253
11378
  const handle = continuation;
10254
- if (!handle || typeof handle.sessionId !== "string") {
10255
- throw new Error("claude-code continue: missing session to resume");
11379
+ if (!handle || typeof handle.threadId !== "string") {
11380
+ throw new Error("codex continue: missing thread to resume");
10256
11381
  }
10257
- const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
10258
- const scratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-resume-"));
11382
+ const codex = requireCodex();
11383
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-codex-resume-"));
10259
11384
  try {
10260
11385
  const spec = {
10261
11386
  rendering: "producer",
10262
11387
  outputLabel: handle.outputLabel,
10263
11388
  outputSchema: handle.outputSchema
10264
11389
  };
10265
- const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId, `${handle.sessionId}-repair`, handle.childEnv === void 0);
11390
+ const { response } = await this.runCodex({
11391
+ codex,
11392
+ args: ["exec", "resume", handle.threadId, ...codexReviewArgs(codex.installDir)],
11393
+ prompt: followUp,
11394
+ spec,
11395
+ cwd: scratch,
11396
+ scratch,
11397
+ model: handle.model,
11398
+ effort: handle.effort,
11399
+ signal,
11400
+ reporter: null,
11401
+ transcriptName: `${handle.threadId}-repair`
11402
+ });
10266
11403
  return response;
10267
11404
  } finally {
10268
11405
  await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
@@ -10270,335 +11407,168 @@ var require_claude_code = __commonJS({
10270
11407
  }
10271
11408
  }
10272
11409
  /**
10273
- * Can this machine run a job now? One real one-turn call through the CLI,
10274
- * the same probe `doctor --deep` makes. The runner asks it only while
10275
- * claiming is paused on an `ExecutorEnvironmentError`, and only in local
10276
- * mode. A signed-out or missing CLI fails before any model call, so the
10277
- * re-check spends nothing until the login works again, and then one turn.
11410
+ * One real turn under the review flags, in a throwaway dir. A signed-out
11411
+ * CLI fails before any model call, so the re-check spends nothing until the
11412
+ * login works again; a spent plan is only visible to a real call.
10278
11413
  */
10279
11414
  async checkReadiness(signal) {
10280
- return new Promise((resolve) => {
10281
- let child;
11415
+ const codex = resolveCodex();
11416
+ if (!codex)
11417
+ return { ready: false, reason: "the codex CLI is not on this machine's PATH" };
11418
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-codex-ready-"));
11419
+ try {
11420
+ let run;
10282
11421
  try {
10283
- child = (0, child_process_1.spawn)("claude", ["--print", "--output-format", "stream-json", "--verbose", "--max-turns", "1", READINESS_PROMPT], { stdio: ["ignore", "pipe", "pipe"] });
11422
+ run = await (0, agent_process_1.runAgentProcess)({
11423
+ bin: codex.bin,
11424
+ args: ["exec", ...codexReviewArgs(codex.installDir), "--ephemeral", "-C", scratch, "-"],
11425
+ cwd: scratch,
11426
+ stdin: READINESS_PROMPT,
11427
+ idleTimeoutMs: READINESS_TIMEOUT_MS,
11428
+ toolName: "the codex CLI",
11429
+ signal
11430
+ });
10284
11431
  } catch (err) {
10285
- resolve({ ready: false, reason: err instanceof Error ? err.message : String(err) });
10286
- return;
10287
- }
10288
- let stdout = "";
10289
- let stderr = "";
10290
- let settled = false;
10291
- const settle = (value) => {
10292
- if (settled)
10293
- return;
10294
- settled = true;
10295
- clearTimeout(timer);
10296
- signal.removeEventListener("abort", onAbort);
10297
- resolve(value);
10298
- };
10299
- const timer = setTimeout(() => {
10300
- child.kill("SIGKILL");
10301
- settle({ ready: false, reason: `the claude CLI did not answer within ${READINESS_TIMEOUT_MS / 1e3}s` });
10302
- }, READINESS_TIMEOUT_MS);
10303
- function onAbort() {
10304
- child.kill("SIGTERM");
10305
- settle({ ready: false, reason: "readiness check aborted" });
10306
- }
10307
- if (signal.aborted) {
10308
- onAbort();
10309
- return;
11432
+ const message = err instanceof Error ? err.message : String(err);
11433
+ return { ready: false, reason: message === "Dispatch aborted" ? "readiness check aborted" : message };
10310
11434
  }
10311
- signal.addEventListener("abort", onAbort, { once: true });
10312
- child.stdout?.on("data", (chunk) => {
10313
- stdout += chunk.toString("utf-8");
10314
- });
10315
- child.stderr?.on("data", (chunk) => {
10316
- if (stderr.length < STDERR_LIMIT)
10317
- stderr += chunk.toString("utf-8");
10318
- });
10319
- child.on("error", (err) => {
10320
- const code = err.code;
10321
- settle({
11435
+ if (run.spawnError) {
11436
+ return {
10322
11437
  ready: false,
10323
- reason: code === "ENOENT" ? `the claude CLI is not on this machine's PATH (${err.message})` : err.message
10324
- });
10325
- });
10326
- child.on("close", (code) => {
10327
- let meta = null;
10328
- try {
10329
- meta = parseStreamJson(stdout).resultMeta;
10330
- } catch {
10331
- meta = null;
10332
- }
10333
- if (code === 0 && !meta?.isError) {
10334
- settle({ ready: true });
10335
- return;
10336
- }
10337
- const said = meta?.detail ?? firstLine(stderr) ?? firstLine(stdout);
10338
- settle({ ready: false, reason: said ?? `the claude CLI exited with code ${code}` });
10339
- });
10340
- });
10341
- }
10342
- runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName, local = false) {
10343
- const outputLabel = spec.outputLabel;
10344
- const model = modelOverride ?? this.config.model;
10345
- const allowedTools = local ? this.config.allowedTools : checkoutScopedTools(this.config.allowedTools);
10346
- if (!local && this.config.claudeSettings === "user") {
10347
- throw new Error("claude-code: `claudeSettings: user` is for an installed Sifter whose login lives in Claude Code settings; a hosted run authenticates with the key it is handed and loads no settings");
10348
- }
10349
- const settingSources = local && this.config.claudeSettings === "user" ? "user" : "";
10350
- return new Promise((resolve, reject) => {
10351
- const args = ["--print", "--output-format", "stream-json", "--verbose", ...reviewRunArgs(settingSources)];
10352
- if (resumeSessionId)
10353
- args.push("--resume", resumeSessionId);
10354
- if (spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0) {
10355
- args.push("--json-schema", JSON.stringify(spec.outputSchema));
10356
- }
10357
- if (model)
10358
- args.push("--model", model);
10359
- if (this.config.maxTurns !== void 0)
10360
- args.push("--max-turns", String(this.config.maxTurns));
10361
- if (allowedTools && allowedTools.length > 0) {
10362
- args.push("--allowedTools", allowedTools.join(","));
10363
- }
10364
- let child;
10365
- try {
10366
- child = (0, child_process_1.spawn)("claude", args, {
10367
- cwd,
10368
- stdio: ["pipe", "pipe", "pipe"],
10369
- // Undefined in local mode → inherit the ambient env (and its
10370
- // `~/.claude` login) unchanged.
10371
- ...childEnv ? { env: childEnv } : {}
10372
- });
10373
- } catch (err) {
10374
- reject(err);
10375
- return;
11438
+ reason: run.spawnError.code === "ENOENT" ? `the codex CLI is not on this machine's PATH (${run.spawnError.message})` : run.spawnError.message
11439
+ };
10376
11440
  }
10377
- const stdoutChunks = [];
10378
- let stderrText = "";
10379
- let settled = false;
10380
- const settle = (fn) => {
10381
- if (settled)
10382
- return;
10383
- settled = true;
10384
- fn();
10385
- };
10386
- const tap = reporter ? new progress_1.StreamJsonTap({
10387
- onReasoning: (t) => reporter.reasoning(t),
10388
- onTool: (name, target) => reporter.tool(name, target),
10389
- onResult: () => reporter.flush()
10390
- }) : null;
10391
- const decoder = tap ? new string_decoder_1.StringDecoder("utf8") : null;
10392
- child.stdin.write(prompt);
10393
- child.stdin.end();
10394
- const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
10395
- let timer;
10396
- const armIdleTimer = () => {
10397
- if (settled)
10398
- return;
10399
- if (timer)
10400
- clearTimeout(timer);
10401
- timer = setTimeout(() => {
10402
- child.kill("SIGTERM");
10403
- settle(() => reject(new Error(`Claude Code produced no output for ${timeout}ms; killed as hung`)));
10404
- }, timeout);
11441
+ const events = parseCodexEvents(run.stdout);
11442
+ if (run.code === 0 && !events.failure)
11443
+ return { ready: true };
11444
+ return {
11445
+ ready: false,
11446
+ reason: events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? `the codex CLI exited with code ${run.code}`
10405
11447
  };
10406
- armIdleTimer();
10407
- child.stdout.on("data", (chunk) => {
10408
- stdoutChunks.push(chunk);
10409
- armIdleTimer();
10410
- if (tap && decoder)
10411
- tap.push(decoder.write(chunk));
10412
- });
10413
- child.stderr?.on("data", (chunk) => {
10414
- if (stderrText.length < STDERR_LIMIT)
10415
- stderrText += chunk.toString("utf-8");
10416
- });
10417
- function onAbort() {
10418
- child.kill("SIGTERM");
10419
- settle(() => reject(new Error("Dispatch aborted")));
10420
- }
10421
- if (signal.aborted) {
10422
- clearTimeout(timer);
10423
- child.kill("SIGTERM");
10424
- settle(() => reject(new Error("Dispatch aborted")));
10425
- return;
10426
- }
10427
- signal.addEventListener("abort", onAbort, { once: true });
10428
- child.on("error", (err) => {
10429
- clearTimeout(timer);
10430
- signal.removeEventListener("abort", onAbort);
10431
- const code = err.code;
10432
- if (local && code === "ENOENT") {
10433
- settle(() => reject(new types_1.ExecutorEnvironmentError(`the claude CLI is not on this machine's PATH (${err.message})`, INSTALL_REMEDY)));
10434
- return;
10435
- }
10436
- settle(() => reject(err));
10437
- });
10438
- child.on("close", (code) => {
10439
- clearTimeout(timer);
10440
- signal.removeEventListener("abort", onAbort);
10441
- if (tap && decoder) {
10442
- tap.push(decoder.end());
10443
- tap.end();
10444
- }
10445
- const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
10446
- if (this.config.keepTranscripts && transcriptName) {
10447
- void keepTranscript((0, apply_1.expandHome)(this.config.keepTranscripts), transcriptName, stdout);
10448
- }
10449
- if (code === 0) {
10450
- try {
10451
- const stream = parseStreamJson(stdout);
10452
- if (stream.resultMeta?.isError) {
10453
- throw runFailure(local, describeClaudeFailure(stream.resultMeta, "Claude Code reported a failure"), {
10454
- apiErrorStatus: stream.resultMeta.apiErrorStatus,
10455
- text: stream.resultMeta.detail ?? firstLine(stderrText)
10456
- });
10457
- }
10458
- const answerText = stream.finalText ?? stdout;
10459
- const parsed = stream.structuredOutput ?? extractJsonObject(answerText);
10460
- if (parsed === void 0 && stream.resultMeta) {
10461
- throw new Error(`claude's final message contained no JSON object (result subtype=${stream.resultMeta.subtype ?? "unknown"}; message head: ${JSON.stringify(answerText.slice(0, 160))})`);
10462
- }
10463
- (0, validate_output_1.assertOutputValid)(spec, parsed);
10464
- settle(() => resolve({
10465
- response: {
10466
- outputs: [{ label: outputLabel, content: parsed ?? { raw: answerText } }],
10467
- rawText: answerText,
10468
- parsed,
10469
- usage: {
10470
- aiProvider: "claude-code",
10471
- ...model ? { aiModel: model } : {},
10472
- ...stream.usage ?? {}
10473
- },
10474
- // In-session repair handle (ADR 0017): present only when the run
10475
- // reported a session id, so the runner can `--resume` it.
10476
- ...stream.sessionId ? {
10477
- continuation: {
10478
- sessionId: stream.sessionId,
10479
- model,
10480
- childEnv,
10481
- outputLabel,
10482
- outputSchema: spec.outputSchema
10483
- }
10484
- } : {}
10485
- },
10486
- readPaths: stream.readPaths,
10487
- toolInputs: stream.toolInputs
10488
- }));
10489
- } catch (err) {
10490
- settle(() => reject(err instanceof Error ? err : new Error(String(err))));
10491
- }
10492
- } else {
10493
- let meta = null;
10494
- try {
10495
- meta = parseStreamJson(stdout).resultMeta;
10496
- } catch {
10497
- }
10498
- const said = meta?.detail ?? firstLine(stderrText) ?? firstLine(stdout);
10499
- const fallback = `Claude Code exited with code ${code}`;
10500
- const failure = runFailure(local, describeClaudeFailure(meta, fallback), {
10501
- apiErrorStatus: meta?.apiErrorStatus ?? null,
10502
- text: said
10503
- });
10504
- if (failure instanceof types_1.ExecutorEnvironmentError && !meta && said) {
10505
- settle(() => reject(new types_1.ExecutorEnvironmentError(`${fallback}: ${said}`, failure.remedy)));
10506
- return;
10507
- }
10508
- settle(() => reject(failure));
10509
- }
11448
+ } finally {
11449
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
10510
11450
  });
10511
- });
10512
- }
10513
- };
10514
- function createClaudeCodeExecutor(config, capabilityId = "claude-code") {
10515
- const validated = { capabilityId };
10516
- if (config.tmpDir !== void 0) {
10517
- if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
10518
- throw new Error('"tmpDir" must be a non-empty string');
10519
- }
10520
- validated.tmpDir = config.tmpDir;
10521
- }
10522
- if (config.model !== void 0) {
10523
- if (typeof config.model !== "string")
10524
- throw new Error('"model" must be a string');
10525
- validated.model = config.model;
10526
- }
10527
- if (config.maxTurns !== void 0) {
10528
- if (typeof config.maxTurns !== "number" || !Number.isInteger(config.maxTurns) || config.maxTurns < 1) {
10529
- throw new Error('"maxTurns" must be a positive integer');
10530
- }
10531
- validated.maxTurns = config.maxTurns;
10532
- }
10533
- if (config.allowedTools !== void 0) {
10534
- if (!Array.isArray(config.allowedTools) || !config.allowedTools.every((t) => typeof t === "string")) {
10535
- throw new Error('"allowedTools" must be an array of strings');
10536
- }
10537
- validated.allowedTools = config.allowedTools;
10538
- }
10539
- if (config.timeout !== void 0) {
10540
- if (typeof config.timeout !== "number" || config.timeout <= 0) {
10541
- throw new Error('"timeout" must be a positive number');
10542
- }
10543
- validated.timeout = config.timeout;
10544
- }
10545
- if (config.keepTranscripts !== void 0) {
10546
- if (typeof config.keepTranscripts !== "string" || config.keepTranscripts.length === 0) {
10547
- throw new Error('"keepTranscripts" must be a non-empty directory path');
10548
- }
10549
- validated.keepTranscripts = config.keepTranscripts;
10550
- }
10551
- if (config.claudeSettings !== void 0) {
10552
- if (config.claudeSettings !== "user") {
10553
- throw new Error('"claudeSettings" may only be "user" (leave it unset to load no settings file)');
10554
11451
  }
10555
- validated.claudeSettings = "user";
10556
11452
  }
10557
- if (config.overlayAllowedHosts !== void 0) {
10558
- if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
10559
- throw new Error('"overlayAllowedHosts" must be an array of strings');
11453
+ /**
11454
+ * The catalog `codex debug models` prints: the models codex itself offers
11455
+ * in its picker, each with the reasoning levels it supports. A `debug`
11456
+ * subcommand, so not a stable contract; any failure or unknown shape
11457
+ * reports nothing.
11458
+ */
11459
+ async listModels(signal) {
11460
+ const codex = resolveCodex();
11461
+ if (!codex)
11462
+ return [];
11463
+ const stdout = await (0, models_1.runListing)(codex.bin, ["debug", "models"], signal);
11464
+ return stdout === null ? [] : parseCodexModels(stdout);
11465
+ }
11466
+ tmpBase() {
11467
+ return this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
11468
+ }
11469
+ async runCodex(opts) {
11470
+ const { spec, model, effort } = opts;
11471
+ const args = [...opts.args];
11472
+ const hasSchema = spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0;
11473
+ if (hasSchema) {
11474
+ const schemaFile = path_1.default.join(opts.scratch, "output-schema.json");
11475
+ await fs_1.promises.writeFile(schemaFile, JSON.stringify((0, strict_schema_1.toStrict)(spec.outputSchema)));
11476
+ args.push("--output-schema", schemaFile);
11477
+ }
11478
+ if (model)
11479
+ args.push("-m", model);
11480
+ if (effort)
11481
+ args.push("-c", `model_reasoning_effort="${effort}"`);
11482
+ args.push("-");
11483
+ const tap = opts.reporter ? codexProgressTap(opts.reporter) : null;
11484
+ const run = await (0, agent_process_1.runAgentProcess)({
11485
+ bin: opts.codex.bin,
11486
+ args,
11487
+ cwd: opts.cwd,
11488
+ stdin: opts.prompt,
11489
+ idleTimeoutMs: this.config.timeout ?? agent_common_1.DEFAULT_AGENT_TIMEOUT,
11490
+ toolName: "Codex",
11491
+ signal: opts.signal,
11492
+ ...tap ? { onStdout: (text) => tap.push(text) } : {}
11493
+ });
11494
+ tap?.end();
11495
+ if (run.spawnError) {
11496
+ if (run.spawnError.code === "ENOENT") {
11497
+ throw new types_1.ExecutorEnvironmentError(`the codex CLI is not on this machine's PATH (${run.spawnError.message})`, CODEX_FAULTS.installRemedy);
11498
+ }
11499
+ throw new Error(run.spawnError.message);
10560
11500
  }
10561
- validated.overlayAllowedHosts = config.overlayAllowedHosts;
10562
- }
10563
- return new ClaudeCodeExecutor(validated);
10564
- }
10565
- function checkoutScopedTools(tools) {
10566
- if (!tools)
10567
- return void 0;
10568
- const scoped = [];
10569
- for (const tool of tools) {
10570
- if (RETIRED_READ_TOOLS.some((t) => tool === t || tool === `${t}(./**)`))
10571
- continue;
10572
- const name = REVIEW_TOOLS.find((t) => tool === t || tool === `${t}(./**)`);
10573
- if (!name) {
10574
- throw new Error(`claude-code: a hosted run may use only ${REVIEW_TOOLS.join(", ")} inside the checkout; "${tool}" is not allowed`);
11501
+ if (this.config.keepTranscripts) {
11502
+ void (0, agent_process_1.keepTranscript)((0, apply_1.expandHome)(this.config.keepTranscripts), opts.transcriptName, run.stdout);
10575
11503
  }
10576
- scoped.push(`${name}(./**)`);
11504
+ const events = parseCodexEvents(run.stdout);
11505
+ if (run.code !== 0 || events.failure) {
11506
+ const said = events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? (0, agent_process_1.firstLine)(run.stdout);
11507
+ const message = said ? `Codex ${run.code === 0 ? "reported a failure" : `exited with code ${run.code}`}: ${said}` : `Codex exited with code ${run.code}`;
11508
+ throw (0, agent_process_1.agentFailure)(true, message, { apiErrorStatus: null, text: said }, CODEX_FAULTS);
11509
+ }
11510
+ if (events.finalText === null) {
11511
+ throw new Error("Codex finished without a final message");
11512
+ }
11513
+ const answerText = events.finalText;
11514
+ const raw = (0, json_answer_1.extractJsonObject)(answerText);
11515
+ if (raw === void 0 && hasSchema) {
11516
+ throw new Error(`codex's final message contained no JSON object (message head: ${JSON.stringify(answerText.slice(0, 160))})`);
11517
+ }
11518
+ const parsed = raw === void 0 ? void 0 : (0, strict_schema_1.dropNulls)(raw, spec.outputSchema);
11519
+ (0, validate_output_1.assertOutputValid)(spec, parsed);
11520
+ return {
11521
+ response: {
11522
+ outputs: [{ label: spec.outputLabel, content: parsed ?? { raw: answerText } }],
11523
+ rawText: answerText,
11524
+ parsed,
11525
+ usage: {
11526
+ aiProvider: "codex",
11527
+ ...model ? { aiModel: model } : {},
11528
+ ...effort ? { aiEffort: effort } : {},
11529
+ ...events.usage ?? {}
11530
+ },
11531
+ ...events.threadId ? {
11532
+ continuation: {
11533
+ threadId: events.threadId,
11534
+ model,
11535
+ effort,
11536
+ outputLabel: spec.outputLabel,
11537
+ outputSchema: spec.outputSchema
11538
+ }
11539
+ } : {}
11540
+ },
11541
+ commands: events.commands
11542
+ };
10577
11543
  }
10578
- return scoped;
11544
+ };
11545
+ function codexProgressTap(reporter) {
11546
+ return new progress_1.JsonLineTap((event) => {
11547
+ const item = event.item;
11548
+ const kind = item ? itemKind(item) : null;
11549
+ if (event.type === "item.started" && kind === "command_execution" && typeof item?.command === "string") {
11550
+ reporter.tool("shell", item.command);
11551
+ } else if (event.type === "item.completed" && kind === "reasoning" && typeof item?.text === "string") {
11552
+ reporter.reasoning(item.text);
11553
+ } else if (event.type === "turn.completed") {
11554
+ reporter.flush();
11555
+ }
11556
+ });
10579
11557
  }
10580
- function buildChildEnv(modelKey, configDir) {
10581
- if (!modelKey && !configDir)
10582
- return void 0;
10583
- const env = {};
10584
- for (const name of HOSTED_ENV_PASSTHROUGH) {
10585
- if (process.env[name] !== void 0)
10586
- env[name] = process.env[name];
10587
- }
10588
- return {
10589
- ...env,
10590
- ...modelKey ? { ANTHROPIC_API_KEY: modelKey } : {},
10591
- ...configDir ? { CLAUDE_CONFIG_DIR: configDir } : {}
10592
- };
11558
+ function itemKind(item) {
11559
+ const kind = typeof item.type === "string" ? item.type : typeof item.item_type === "string" ? item.item_type : null;
11560
+ return kind === "assistant_message" ? "agent_message" : kind;
10593
11561
  }
10594
- function parseStreamJson(stdout) {
11562
+ function parseCodexEvents(stdout) {
11563
+ let threadId = null;
10595
11564
  let finalText = null;
10596
- const readPaths = [];
10597
- let usage = null;
10598
- let resultMeta = null;
10599
- let structuredOutput = null;
10600
- let sessionId = null;
10601
- const toolInputs = [];
11565
+ const commands = [];
11566
+ const seenCommands = /* @__PURE__ */ new Set();
11567
+ let input = 0;
11568
+ let output = 0;
11569
+ let sawUsage = false;
11570
+ let turnFailure = null;
11571
+ let lastError = null;
10602
11572
  for (const line of stdout.split("\n")) {
10603
11573
  const trimmed = line.trim();
10604
11574
  if (!trimmed.startsWith("{"))
@@ -10609,257 +11579,580 @@ var require_claude_code = __commonJS({
10609
11579
  } catch {
10610
11580
  continue;
10611
11581
  }
10612
- if (typeof event.session_id === "string")
10613
- sessionId = event.session_id;
10614
- if (event.type === "assistant") {
10615
- const content = event.message?.content;
10616
- if (!Array.isArray(content))
10617
- continue;
10618
- for (const block of content) {
10619
- const b = block;
10620
- if (b?.type !== "tool_use")
10621
- continue;
10622
- const fp = b.input?.file_path;
10623
- if (b.name === "Read" && typeof fp === "string") {
10624
- readPaths.push(fp);
11582
+ switch (event.type) {
11583
+ case "thread.started":
11584
+ if (typeof event.thread_id === "string")
11585
+ threadId = event.thread_id;
11586
+ break;
11587
+ case "item.started":
11588
+ case "item.updated":
11589
+ case "item.completed": {
11590
+ const item = event.item;
11591
+ if (!item)
11592
+ break;
11593
+ const kind = itemKind(item);
11594
+ if (kind === "agent_message" && event.type === "item.completed" && typeof item.text === "string") {
11595
+ finalText = item.text;
11596
+ lastError = null;
11597
+ } else if (kind === "command_execution" && typeof item.command === "string") {
11598
+ const key = typeof item.id === "string" ? item.id : item.command;
11599
+ if (!seenCommands.has(key)) {
11600
+ seenCommands.add(key);
11601
+ commands.push(item.command);
11602
+ }
10625
11603
  }
10626
- if (b.input && typeof b.input === "object")
10627
- toolInputs.push(JSON.stringify(b.input));
11604
+ break;
10628
11605
  }
10629
- } else if (event.type === "result") {
10630
- if (typeof event.result === "string" && event.result.trim().length > 0) {
10631
- finalText = event.result;
11606
+ case "turn.completed": {
11607
+ const u = event.usage;
11608
+ if (typeof u?.input_tokens === "number") {
11609
+ input += u.input_tokens;
11610
+ sawUsage = true;
11611
+ }
11612
+ if (typeof u?.output_tokens === "number") {
11613
+ output += u.output_tokens;
11614
+ sawUsage = true;
11615
+ }
11616
+ break;
10632
11617
  }
10633
- resultMeta = {
10634
- subtype: typeof event.subtype === "string" ? event.subtype : null,
10635
- isError: event.is_error === true,
10636
- // On a failure the CLI puts a human-readable reason in `result` — the
10637
- // model was unavailable, spend limit hit, and so on. It is the only
10638
- // place that reason exists, so keep it for the failure message.
10639
- detail: typeof event.result === "string" && event.result.trim().length > 0 ? event.result.trim() : null,
10640
- apiErrorStatus: typeof event.api_error_status === "number" ? event.api_error_status : null
10641
- };
10642
- const so = event.structured_output;
10643
- if (so && typeof so === "object" && !Array.isArray(so)) {
10644
- structuredOutput = so;
11618
+ case "turn.failed": {
11619
+ const err = event.error;
11620
+ turnFailure = typeof err?.message === "string" ? err.message : "the turn failed";
11621
+ break;
10645
11622
  }
10646
- const u = event.usage;
10647
- const cost = event.total_cost_usd;
10648
- usage = {
10649
- ...typeof u?.input_tokens === "number" ? { inputTokens: u.input_tokens } : {},
10650
- ...typeof u?.output_tokens === "number" ? { outputTokens: u.output_tokens } : {},
10651
- ...typeof cost === "number" ? { costUsd: cost } : {}
10652
- };
11623
+ case "error":
11624
+ if (typeof event.message === "string")
11625
+ lastError = event.message;
11626
+ break;
11627
+ default:
11628
+ break;
10653
11629
  }
10654
11630
  }
10655
- return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, toolInputs };
11631
+ return {
11632
+ threadId,
11633
+ finalText,
11634
+ commands,
11635
+ usage: sawUsage ? { inputTokens: input, outputTokens: output } : null,
11636
+ failure: turnFailure ?? (finalText === null ? lastError : null)
11637
+ };
10656
11638
  }
10657
- var MAX_REPORTED_READS = 2e3;
10658
- function workspaceRoots(cwd) {
10659
- const root = path_1.default.resolve(cwd);
10660
- const roots = [root];
11639
+ function parseCodexModels(stdout) {
11640
+ let body;
10661
11641
  try {
10662
- const real = (0, fs_1.realpathSync)(root);
10663
- if (real !== root)
10664
- roots.push(real);
11642
+ body = JSON.parse(stdout);
10665
11643
  } catch {
11644
+ return [];
10666
11645
  }
10667
- return roots;
11646
+ const entries = body?.models;
11647
+ if (!Array.isArray(entries))
11648
+ return [];
11649
+ const listed = entries.flatMap((entry) => {
11650
+ if (!entry || typeof entry !== "object")
11651
+ return [];
11652
+ const e = entry;
11653
+ if (typeof e.slug !== "string" || e.slug.length === 0 || e.visibility !== "list")
11654
+ return [];
11655
+ const levels = Array.isArray(e.supported_reasoning_levels) ? e.supported_reasoning_levels.flatMap((level) => {
11656
+ const effort = level?.effort;
11657
+ return typeof effort === "string" && /^[a-z]+$/.test(effort) ? [effort] : [];
11658
+ }) : [];
11659
+ const model = {
11660
+ id: e.slug,
11661
+ label: typeof e.display_name === "string" && e.display_name ? e.display_name : e.slug,
11662
+ ...levels.length > 0 ? { efforts: levels } : {}
11663
+ };
11664
+ return [{ model, priority: typeof e.priority === "number" ? e.priority : Number.MAX_SAFE_INTEGER }];
11665
+ });
11666
+ return listed.sort((a, b) => a.priority - b.priority).map((entry) => entry.model);
10668
11667
  }
10669
- function relativeWorkspaceReads(readPaths, cwd) {
10670
- const roots = workspaceRoots(cwd);
10671
- const root = roots[0];
10672
- const out = /* @__PURE__ */ new Set();
10673
- for (const p of readPaths) {
10674
- const resolved = path_1.default.resolve(root, p);
10675
- for (const r of roots) {
10676
- if (resolved === r)
10677
- break;
10678
- if (!resolved.startsWith(r + path_1.default.sep))
10679
- continue;
10680
- out.add(resolved.slice(r.length + 1));
10681
- break;
10682
- }
10683
- if (out.size >= MAX_REPORTED_READS)
10684
- break;
10685
- }
10686
- return [...out].sort();
11668
+ function createCodexExecutor(config, capabilityId = "codex") {
11669
+ const validated = (0, agent_common_1.parseAgentCommonConfig)(config, capabilityId);
11670
+ const effort = (0, models_1.parseReasoningEffort)(config.reasoningEffort, models_1.REASONING_EFFORTS);
11671
+ if (effort)
11672
+ validated.reasoningEffort = effort;
11673
+ return new CodexExecutor(validated);
10687
11674
  }
10688
- var GLOB_CHARS = /[*?[{]/;
10689
- function escapeRegExp(s) {
10690
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11675
+ }
11676
+ });
11677
+
11678
+ // ../../packages/shuttle/dist/executors/copilot.js
11679
+ var require_copilot = __commonJS({
11680
+ "../../packages/shuttle/dist/executors/copilot.js"(exports2) {
11681
+ "use strict";
11682
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
11683
+ return mod && mod.__esModule ? mod : { "default": mod };
11684
+ };
11685
+ Object.defineProperty(exports2, "__esModule", { value: true });
11686
+ exports2.copilotReviewArgs = copilotReviewArgs;
11687
+ exports2.withSchema = withSchema;
11688
+ exports2.usageOf = usageOf;
11689
+ exports2.parseCopilotEvents = parseCopilotEvents;
11690
+ exports2.parseCopilotModels = parseCopilotModels;
11691
+ exports2.createCopilotExecutor = createCopilotExecutor;
11692
+ var crypto_1 = require("crypto");
11693
+ var fs_1 = require("fs");
11694
+ var os_1 = __importDefault(require("os"));
11695
+ var path_1 = __importDefault(require("path"));
11696
+ var loom_1 = require_dist();
11697
+ var types_1 = require_types2();
11698
+ var validate_output_1 = require_validate_output();
11699
+ var progress_1 = require_progress();
11700
+ var agent_process_1 = require_agent_process();
11701
+ var agent_common_1 = require_agent_common();
11702
+ var json_answer_1 = require_json_answer();
11703
+ var models_1 = require_models();
11704
+ var workspace_telemetry_1 = require_workspace_telemetry();
11705
+ var apply_1 = require_apply();
11706
+ var REVIEW_TOOLS = ["view", "grep", "glob"];
11707
+ var DENIED_TOOLS = ["shell", "write"];
11708
+ function copilotReviewArgs() {
11709
+ return [
11710
+ "--output-format",
11711
+ "json",
11712
+ "--no-color",
11713
+ "--allow-all-tools",
11714
+ `--available-tools=${REVIEW_TOOLS.join(",")}`,
11715
+ ...DENIED_TOOLS.map((t) => `--deny-tool=${t}`),
11716
+ "--disable-builtin-mcps",
11717
+ "--disallow-temp-dir",
11718
+ "--no-ask-user",
11719
+ "--no-auto-update",
11720
+ "--no-remote"
11721
+ ];
10691
11722
  }
10692
- function globToRegExp(glob) {
10693
- let re = "^";
10694
- for (let i = 0; i < glob.length; i++) {
10695
- const ch = glob[i];
10696
- if (ch === "*") {
10697
- if (glob[i + 1] === "*") {
10698
- if (glob[i + 2] === "/") {
10699
- re += "(?:.*/)?";
10700
- i += 2;
10701
- } else {
10702
- re += ".*";
10703
- i += 1;
10704
- }
10705
- } else {
10706
- re += "[^/]*";
10707
- }
10708
- } else if (ch === "?") {
10709
- re += "[^/]";
10710
- } else if (ch === "[") {
10711
- const end = glob.indexOf("]", i + 1);
10712
- if (end === -1)
10713
- return null;
10714
- let cls = glob.slice(i + 1, end);
10715
- if (cls.startsWith("!"))
10716
- cls = `^${cls.slice(1)}`;
10717
- re += `[${cls.replace(/\\/g, "\\\\")}]`;
10718
- i = end;
10719
- } else if (ch === "{") {
10720
- const end = glob.indexOf("}", i + 1);
10721
- if (end === -1)
10722
- return null;
10723
- re += `(?:${glob.slice(i + 1, end).split(",").map(escapeRegExp).join("|")})`;
10724
- i = end;
10725
- } else {
10726
- re += escapeRegExp(ch);
10727
- }
11723
+ var RUN_SETTINGS = { disableAllHooks: true, autoUpdate: false };
11724
+ var COPILOT_FAULTS = {
11725
+ installRemedy: "install GitHub Copilot CLI (brew install --cask copilot-cli, or npm install -g @github/copilot) where this process can find it on PATH",
11726
+ loginRemedy: "run `copilot login` and sign in with a GitHub account that has a Copilot seat (a COPILOT_GITHUB_TOKEN, GH_TOKEN or GITHUB_TOKEN in the environment wins over that login)",
11727
+ limitRemedy: "the account has no Copilot allowance left: on an organization seat, an owner must allow paid usage in the organization's Copilot policies and fund a budget for it; otherwise wait for the monthly reset",
11728
+ signedOut: /not (?:logged|signed) in|run `?copilot login|no authentication|authentication (?:failed|required)|bad credentials/i,
11729
+ outOfUsage: /exceeded your monthly quota|premium request|usage limit|hit your (?:\w+ )?limit|exceeded .*(?:allowance|limit)/i,
11730
+ notEntitled: {
11731
+ pattern: /access denied by policy|not authorized to use this copilot feature/i,
11732
+ remedy: "this account's Copilot does not allow the Copilot CLI: on an organization seat, an owner must assign the seat and enable the Copilot CLI policy, then run `copilot login` again so the new seat is picked up"
10728
11733
  }
10729
- return new RegExp(`${re}$`);
10730
- }
10731
- function creditOverlayReads(toolInputs, writtenPaths, runCwd) {
10732
- if (writtenPaths.length === 0 || toolInputs.length === 0)
10733
- return [];
10734
- const roots = runCwd ? workspaceRoots(runCwd) : [];
10735
- const dirs = /* @__PURE__ */ new Set();
10736
- for (const rel of writtenPaths) {
10737
- const parts = rel.split("/");
10738
- for (let i = 1; i < parts.length; i++)
10739
- dirs.add(`${parts.slice(0, i).join("/")}/`);
11734
+ };
11735
+ var READINESS_PROMPT = "Reply with the single word: ok";
11736
+ var READINESS_TIMEOUT_MS = 12e4;
11737
+ var CopilotExecutor = class {
11738
+ capability;
11739
+ strategies = [loom_1.PROMPT_EXECUTION_STRATEGY];
11740
+ config;
11741
+ constructor(config) {
11742
+ this.config = config;
11743
+ this.capability = { id: config.capabilityId };
10740
11744
  }
10741
- const dirAlt = [...dirs].map(escapeRegExp).join("|");
10742
- const wordRe = dirs.size > 0 ? new RegExp(`(?:${dirAlt})[^\\s"'\`;|&<>()\\\\]*`, "g") : null;
10743
- const findRe = dirs.size > 0 ? new RegExp(`\\bfind\\s+(?:\\./)?(${[...dirs].map((d) => escapeRegExp(d.slice(0, -1))).join("|")})/?(?=\\s|$)`, "g") : null;
10744
- const readerRe = new RegExp(`(?:^|[\\s"])(?:-exec|-execdir|xargs)\\b[^;|&]*\\b${READERS}\\b`);
10745
- const plainReaderRe = new RegExp(`(?:^|[\\s"'\`;|&(])${READERS}\\b`);
10746
- const credited = /* @__PURE__ */ new Set();
10747
- for (const raw of toolInputs) {
10748
- const input = roots.reduce((s, r) => s.split(`${r}/`).join(""), raw);
10749
- for (const rel of writtenPaths) {
10750
- if (input.includes(rel))
10751
- credited.add(rel);
11745
+ estimate(dispatch) {
11746
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
11747
+ return {
11748
+ inputTokens: spec.costCapHint?.estimatedInputTokens,
11749
+ outputTokens: spec.costCapHint?.estimatedOutputTokens
11750
+ };
11751
+ }
11752
+ async prepareEnvironment(dispatch) {
11753
+ return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
11754
+ }
11755
+ async execute(dispatch, signal, ctx) {
11756
+ (0, agent_common_1.refuseHosted)("copilot", Boolean(ctx?.credentials));
11757
+ const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
11758
+ const workspace = ctx?.workspace ?? null;
11759
+ const hints = (0, loom_1.pickProviderHints)(spec, "copilot");
11760
+ const model = typeof hints.model === "string" && hints.model.length > 0 ? hints.model : this.config.model;
11761
+ const effort = (0, models_1.hintedEffort)(hints) ?? this.config.reasoningEffort;
11762
+ const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
11763
+ const runs = path_1.default.join(this.tmpBase(), "shuttle-copilot-runs");
11764
+ await fs_1.promises.mkdir(runs, { recursive: true, mode: 448 });
11765
+ await pruneRuns(runs);
11766
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(runs, "run-"));
11767
+ let keep = false;
11768
+ try {
11769
+ const home = await makeRunHome(scratch);
11770
+ let runCwd = workspace?.cwd;
11771
+ if (!runCwd) {
11772
+ runCwd = path_1.default.join(scratch, "run");
11773
+ await fs_1.promises.mkdir(runCwd);
11774
+ }
11775
+ const sessionId = (0, crypto_1.randomUUID)();
11776
+ const { response, reads, toolInputs } = await this.runCopilot({
11777
+ args: [...copilotReviewArgs(), `--session-id=${sessionId}`, "-C", runCwd],
11778
+ prompt: withSchema(prompt, spec),
11779
+ spec,
11780
+ cwd: runCwd,
11781
+ home,
11782
+ scratch,
11783
+ model,
11784
+ effort,
11785
+ signal,
11786
+ reporter,
11787
+ transcriptName: dispatch.job.id
11788
+ });
11789
+ response.continuation = {
11790
+ sessionId,
11791
+ home,
11792
+ model,
11793
+ effort,
11794
+ outputLabel: spec.outputLabel,
11795
+ outputSchema: spec.outputSchema
11796
+ };
11797
+ keep = true;
11798
+ if (workspace)
11799
+ response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, reads, toolInputs));
11800
+ return response;
11801
+ } finally {
11802
+ reporter?.close();
11803
+ if (!keep)
11804
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
11805
+ });
10752
11806
  }
10753
- if (wordRe) {
10754
- for (const match of input.matchAll(wordRe)) {
10755
- const word = match[0];
10756
- if (!GLOB_CHARS.test(word))
10757
- continue;
10758
- const re = globToRegExp(word);
10759
- if (!re)
10760
- continue;
10761
- for (const rel of writtenPaths) {
10762
- if (re.test(rel))
10763
- credited.add(rel);
10764
- }
10765
- }
11807
+ }
11808
+ /** In-session gate repair (ADR 0017): resume the session this executor named, from the run's own COPILOT_HOME. */
11809
+ async continue(continuation, followUp, signal, ctx) {
11810
+ (0, agent_common_1.refuseHosted)("copilot", Boolean(ctx?.credentials));
11811
+ const handle = continuation;
11812
+ if (!handle || typeof handle.sessionId !== "string" || typeof handle.home !== "string") {
11813
+ throw new Error("copilot continue: missing session to resume");
10766
11814
  }
10767
- if (findRe && readerRe.test(input)) {
10768
- for (const match of input.matchAll(findRe)) {
10769
- const dir = `${match[1]}/`;
10770
- for (const rel of writtenPaths) {
10771
- if (rel.startsWith(dir))
10772
- credited.add(rel);
10773
- }
11815
+ const scratch = path_1.default.dirname(handle.home);
11816
+ const cwd = path_1.default.join(scratch, "resume");
11817
+ await fs_1.promises.mkdir(cwd, { recursive: true });
11818
+ const spec = {
11819
+ rendering: "producer",
11820
+ outputLabel: handle.outputLabel,
11821
+ outputSchema: handle.outputSchema
11822
+ };
11823
+ const { response } = await this.runCopilot({
11824
+ args: [...copilotReviewArgs(), `--resume=${handle.sessionId}`, "-C", cwd],
11825
+ prompt: followUp,
11826
+ spec,
11827
+ cwd,
11828
+ home: handle.home,
11829
+ scratch,
11830
+ model: handle.model,
11831
+ effort: handle.effort,
11832
+ signal,
11833
+ reporter: null,
11834
+ transcriptName: `${handle.sessionId}-repair`
11835
+ });
11836
+ response.continuation = handle;
11837
+ return response;
11838
+ }
11839
+ /** One real turn under the review flags. A signed-out CLI fails before any model call. */
11840
+ async checkReadiness(signal) {
11841
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-copilot-ready-"));
11842
+ try {
11843
+ const home = await makeRunHome(scratch);
11844
+ let run;
11845
+ try {
11846
+ run = await withOperatorLogin(home, () => (0, agent_process_1.runAgentProcess)({
11847
+ bin: "copilot",
11848
+ args: [...copilotReviewArgs(), "-C", scratch, "-p", READINESS_PROMPT],
11849
+ cwd: scratch,
11850
+ env: runEnv(home),
11851
+ idleTimeoutMs: READINESS_TIMEOUT_MS,
11852
+ toolName: "the copilot CLI",
11853
+ signal
11854
+ }));
11855
+ } catch (err) {
11856
+ const message = err instanceof Error ? err.message : String(err);
11857
+ return { ready: false, reason: message === "Dispatch aborted" ? "readiness check aborted" : message };
10774
11858
  }
10775
- }
10776
- if (!plainReaderRe.test(input))
10777
- continue;
10778
- for (const match of input.matchAll(CD_RE)) {
10779
- const target = match[1].replace(/^\.\//, "").replace(/\/+$/, "");
10780
- const base = target.length > 0 ? `${target}/` : "";
10781
- for (const rel of writtenPaths) {
10782
- if (!rel.startsWith(base))
10783
- continue;
10784
- if (namesBareWord(input, rel.slice(base.length)))
10785
- credited.add(rel);
11859
+ if (run.spawnError) {
11860
+ return {
11861
+ ready: false,
11862
+ reason: run.spawnError.code === "ENOENT" ? `the copilot CLI is not on this machine's PATH (${run.spawnError.message})` : run.spawnError.message
11863
+ };
10786
11864
  }
11865
+ const events = parseCopilotEvents(run.stdout);
11866
+ if (run.code === 0 && !events.failure)
11867
+ return { ready: true };
11868
+ return {
11869
+ ready: false,
11870
+ reason: events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? `the copilot CLI exited with code ${run.code}`
11871
+ };
11872
+ } finally {
11873
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
11874
+ });
10787
11875
  }
10788
11876
  }
10789
- return [...credited].sort();
11877
+ /**
11878
+ * The `model` setting's values as `copilot help config` lists them: the
11879
+ * CLI's built-in set. It does not reflect an organization's model policy,
11880
+ * so a model listed here can still be refused for one account, and that
11881
+ * review fails in copilot's own words. Each takes the CLI-wide effort list.
11882
+ */
11883
+ async listModels(signal) {
11884
+ const stdout = await (0, models_1.runListing)("copilot", ["help", "config"], signal);
11885
+ return stdout === null ? [] : parseCopilotModels(stdout);
11886
+ }
11887
+ tmpBase() {
11888
+ return this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
11889
+ }
11890
+ async runCopilot(opts) {
11891
+ const { spec, model, effort } = opts;
11892
+ const usageFile = path_1.default.join(opts.scratch, `usage-${(0, crypto_1.randomUUID)()}.json`);
11893
+ const args = [...opts.args, `--usage-output-file=${usageFile}`, `--log-dir=${path_1.default.join(opts.home, "logs")}`];
11894
+ if (model)
11895
+ args.push(`--model=${model}`);
11896
+ if (effort)
11897
+ args.push(`--reasoning-effort=${effort}`);
11898
+ const tap = opts.reporter ? copilotProgressTap(opts.reporter) : null;
11899
+ const run = await withOperatorLogin(opts.home, () => (0, agent_process_1.runAgentProcess)({
11900
+ bin: "copilot",
11901
+ args,
11902
+ cwd: opts.cwd,
11903
+ env: runEnv(opts.home),
11904
+ // The prompt comes from stdin: a review prompt carries the diff, far
11905
+ // past what argv should hold, and argv is visible to every process.
11906
+ stdin: opts.prompt,
11907
+ idleTimeoutMs: this.config.timeout ?? agent_common_1.DEFAULT_AGENT_TIMEOUT,
11908
+ toolName: "Copilot",
11909
+ signal: opts.signal,
11910
+ ...tap ? { onStdout: (text) => tap.push(text) } : {}
11911
+ }));
11912
+ tap?.end();
11913
+ if (run.spawnError) {
11914
+ if (run.spawnError.code === "ENOENT") {
11915
+ throw new types_1.ExecutorEnvironmentError(`the copilot CLI is not on this machine's PATH (${run.spawnError.message})`, COPILOT_FAULTS.installRemedy);
11916
+ }
11917
+ throw new Error(run.spawnError.message);
11918
+ }
11919
+ if (this.config.keepTranscripts) {
11920
+ void (0, agent_process_1.keepTranscript)((0, apply_1.expandHome)(this.config.keepTranscripts), opts.transcriptName, run.stdout);
11921
+ }
11922
+ const events = parseCopilotEvents(run.stdout);
11923
+ if (run.code !== 0 || events.failure) {
11924
+ const said = events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? (0, agent_process_1.firstLine)(run.stdout);
11925
+ const message = said ? `Copilot ${run.code === 0 ? "reported a failure" : `exited with code ${run.code}`}: ${said}` : `Copilot exited with code ${run.code}`;
11926
+ throw (0, agent_process_1.agentFailure)(true, message, { apiErrorStatus: null, text: said }, COPILOT_FAULTS);
11927
+ }
11928
+ if (events.finalText === null)
11929
+ throw new Error("Copilot finished without a final message");
11930
+ const answerText = events.finalText;
11931
+ const parsed = (0, json_answer_1.extractJsonObject)(answerText);
11932
+ if (parsed === void 0 && hasSchema(spec)) {
11933
+ throw new Error(`copilot's final message contained no JSON object (message head: ${JSON.stringify(answerText.slice(0, 160))})`);
11934
+ }
11935
+ (0, validate_output_1.assertOutputValid)(spec, parsed);
11936
+ const usage = await readUsage(usageFile);
11937
+ return {
11938
+ response: {
11939
+ outputs: [{ label: spec.outputLabel, content: parsed ?? { raw: answerText } }],
11940
+ rawText: answerText,
11941
+ parsed,
11942
+ // The model the session actually ran, from its usage file, over the one asked for.
11943
+ usage: {
11944
+ aiProvider: "copilot",
11945
+ ...model ? { aiModel: model } : {},
11946
+ ...effort ? { aiEffort: effort } : {},
11947
+ ...usage ?? {}
11948
+ }
11949
+ },
11950
+ reads: events.readPaths,
11951
+ toolInputs: events.toolInputs
11952
+ };
11953
+ }
11954
+ };
11955
+ function hasSchema(spec) {
11956
+ return Boolean(spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0);
10790
11957
  }
10791
- var READERS = "(?:cat|head|tail|sed|awk|less|more|bat)";
10792
- var CD_RE = /(?:^|[\s"'`;|&(])cd\s+(?:--\s+)?["']?([^\s"'`;|&<>()\\]+)/g;
10793
- function namesBareWord(input, word) {
10794
- const edge = String.raw`[\s"'\`;|&<>(),\\]`;
10795
- return new RegExp(`(?:^|${edge})${escapeRegExp(word)}(?=$|${edge})`).test(input);
11958
+ function withSchema(prompt, spec) {
11959
+ if (!hasSchema(spec))
11960
+ return prompt;
11961
+ return `${prompt}
11962
+
11963
+ ---
11964
+
11965
+ Your final message must be one JSON object, and nothing else, that validates against this JSON Schema:
11966
+
11967
+ ${JSON.stringify(spec.outputSchema)}
11968
+ `;
10796
11969
  }
10797
- async function keepTranscript(dir, name, stdout) {
11970
+ var RUN_RETENTION_MS = 24 * 60 * 60 * 1e3;
11971
+ async function pruneRuns(runs) {
11972
+ let entries;
10798
11973
  try {
10799
- await fs_1.promises.mkdir(dir, { recursive: true });
10800
- const safe = name.replace(/[^A-Za-z0-9._-]/g, "_");
10801
- await fs_1.promises.writeFile(path_1.default.join(dir, `${safe}.jsonl`), stdout);
11974
+ entries = await fs_1.promises.readdir(runs);
11975
+ } catch {
11976
+ return;
11977
+ }
11978
+ const cutoff = Date.now() - RUN_RETENTION_MS;
11979
+ await Promise.all(entries.map(async (name) => {
11980
+ const dir = path_1.default.join(runs, name);
11981
+ try {
11982
+ if ((await fs_1.promises.stat(dir)).mtimeMs < cutoff)
11983
+ await fs_1.promises.rm(dir, { recursive: true, force: true });
11984
+ } catch {
11985
+ }
11986
+ }));
11987
+ }
11988
+ async function makeRunHome(scratch) {
11989
+ const home = path_1.default.join(scratch, "copilot-home");
11990
+ await fs_1.promises.mkdir(home, { mode: 448 });
11991
+ await fs_1.promises.writeFile(path_1.default.join(home, "settings.json"), JSON.stringify(RUN_SETTINGS));
11992
+ const bin = path_1.default.join(scratch, "bin");
11993
+ await fs_1.promises.mkdir(bin, { recursive: true });
11994
+ await fs_1.promises.writeFile(path_1.default.join(bin, "gh"), REFUSING_GH, { mode: 493 });
11995
+ return home;
11996
+ }
11997
+ var REFUSING_GH = '#!/bin/sh\necho "gh is not available to a Sifter review" >&2\nexit 1\n';
11998
+ var LOGIN_POINTER_KEYS = ["lastLoggedInUser", "loggedInUsers"];
11999
+ var LOGIN_TOKEN_KEYS = ["authTokens"];
12000
+ async function withOperatorLogin(home, fn) {
12001
+ const file = path_1.default.join(home, "config.json");
12002
+ const login = await operatorLogin();
12003
+ if (login)
12004
+ await fs_1.promises.writeFile(file, JSON.stringify(login), { mode: 384 });
12005
+ else
12006
+ await fs_1.promises.rm(file, { force: true });
12007
+ try {
12008
+ return await fn();
12009
+ } finally {
12010
+ await forgetToken(file);
12011
+ }
12012
+ }
12013
+ async function forgetToken(file) {
12014
+ let config;
12015
+ try {
12016
+ config = await readJsonc(file);
10802
12017
  } catch (err) {
10803
- console.warn(`[shuttle] could not keep the transcript for ${name} under ${dir}: ${err instanceof Error ? err.message : String(err)}`);
12018
+ if (err.code !== "ENOENT")
12019
+ await fs_1.promises.rm(file, { force: true }).catch(() => {
12020
+ });
12021
+ return;
10804
12022
  }
12023
+ if (!LOGIN_TOKEN_KEYS.some((k) => k in config))
12024
+ return;
12025
+ for (const k of LOGIN_TOKEN_KEYS)
12026
+ delete config[k];
12027
+ await fs_1.promises.writeFile(file, JSON.stringify(config), { mode: 384 }).catch(() => fs_1.promises.rm(file, { force: true }));
10805
12028
  }
10806
- function extractJsonObject(s) {
10807
- const exact = tryParseObject(s);
10808
- if (exact)
10809
- return exact;
10810
- let last;
10811
- let i = 0;
10812
- while (i < s.length) {
10813
- if (s[i] !== "{") {
10814
- i++;
12029
+ async function operatorLogin() {
12030
+ const home = process.env.COPILOT_HOME ?? path_1.default.join(os_1.default.homedir(), ".copilot");
12031
+ try {
12032
+ const parsed = await readJsonc(path_1.default.join(home, "config.json"));
12033
+ const keys = [...LOGIN_POINTER_KEYS, ...LOGIN_TOKEN_KEYS].filter((k) => k in parsed);
12034
+ return keys.length > 0 ? Object.fromEntries(keys.map((k) => [k, parsed[k]])) : null;
12035
+ } catch {
12036
+ return null;
12037
+ }
12038
+ }
12039
+ async function readJsonc(file) {
12040
+ const raw = await fs_1.promises.readFile(file, "utf8");
12041
+ return JSON.parse(raw.split("\n").filter((line) => !line.trim().startsWith("//")).join("\n"));
12042
+ }
12043
+ function runEnv(home) {
12044
+ const bin = path_1.default.join(path_1.default.dirname(home), "bin");
12045
+ return {
12046
+ ...process.env,
12047
+ PATH: `${bin}${path_1.default.delimiter}${process.env.PATH ?? ""}`,
12048
+ COPILOT_HOME: home,
12049
+ COPILOT_AUTO_UPDATE: "false"
12050
+ };
12051
+ }
12052
+ async function readUsage(file) {
12053
+ try {
12054
+ const raw = JSON.parse(await fs_1.promises.readFile(file, "utf8"));
12055
+ return usageOf(raw);
12056
+ } catch {
12057
+ return null;
12058
+ }
12059
+ }
12060
+ function usageOf(raw) {
12061
+ const models = Object.entries(raw.modelMetrics ?? {});
12062
+ if (models.length === 0)
12063
+ return null;
12064
+ const num = (v) => typeof v === "number" ? v : 0;
12065
+ let input = 0;
12066
+ let output = 0;
12067
+ for (const [, m] of models) {
12068
+ input += num(m.usage?.inputTokens) + num(m.usage?.cacheReadTokens);
12069
+ output += num(m.usage?.outputTokens);
12070
+ }
12071
+ return { inputTokens: input, outputTokens: output, ...models.length === 1 ? { aiModel: models[0][0] } : {} };
12072
+ }
12073
+ function copilotProgressTap(reporter) {
12074
+ return new progress_1.JsonLineTap((event) => {
12075
+ const data = event.data ?? {};
12076
+ if (event.type === "tool.execution_start" && typeof data.toolName === "string") {
12077
+ const args = data.arguments ?? {};
12078
+ const target = [args.path, args.pattern].find((v) => typeof v === "string");
12079
+ reporter.tool(data.toolName, target);
12080
+ } else if (event.type === "assistant.reasoning" && typeof data.content === "string") {
12081
+ reporter.reasoning(data.content);
12082
+ } else if (event.type === "assistant.turn_end") {
12083
+ reporter.flush();
12084
+ }
12085
+ });
12086
+ }
12087
+ function parseCopilotEvents(stdout) {
12088
+ let finalText = null;
12089
+ const readPaths = [];
12090
+ const toolInputs = [];
12091
+ const started = /* @__PURE__ */ new Map();
12092
+ let lastError = null;
12093
+ for (const line of stdout.split("\n")) {
12094
+ const trimmed = line.trim();
12095
+ if (!trimmed.startsWith("{"))
12096
+ continue;
12097
+ let event;
12098
+ try {
12099
+ event = JSON.parse(trimmed);
12100
+ } catch {
10815
12101
  continue;
10816
12102
  }
10817
- let depth = 0;
10818
- let inString = false;
10819
- let escaped = false;
10820
- let end = -1;
10821
- for (let j = i; j < s.length; j++) {
10822
- const ch = s[j];
10823
- if (escaped) {
10824
- escaped = false;
10825
- continue;
10826
- }
10827
- if (ch === "\\") {
10828
- escaped = inString;
10829
- continue;
10830
- }
10831
- if (ch === '"') {
10832
- inString = !inString;
10833
- continue;
10834
- }
10835
- if (inString)
10836
- continue;
10837
- if (ch === "{")
10838
- depth++;
10839
- else if (ch === "}") {
10840
- depth--;
10841
- if (depth === 0) {
10842
- end = j;
12103
+ const data = event.data ?? {};
12104
+ switch (event.type) {
12105
+ case "tool.execution_start": {
12106
+ if (typeof data.toolCallId !== "string" || typeof data.toolName !== "string")
10843
12107
  break;
12108
+ const args = data.arguments ?? {};
12109
+ started.set(data.toolCallId, { name: data.toolName, args });
12110
+ toolInputs.push(JSON.stringify(args));
12111
+ break;
12112
+ }
12113
+ case "tool.execution_complete": {
12114
+ const call = typeof data.toolCallId === "string" ? started.get(data.toolCallId) : void 0;
12115
+ if (call && data.success === true && call.name === "view" && typeof call.args.path === "string") {
12116
+ readPaths.push(call.args.path);
10844
12117
  }
12118
+ break;
10845
12119
  }
12120
+ case "assistant.message":
12121
+ if (typeof data.content === "string" && data.content.trim().length > 0) {
12122
+ finalText = data.content;
12123
+ lastError = null;
12124
+ }
12125
+ break;
12126
+ default:
12127
+ if (typeof event.type === "string" && /(^|\.)error$/.test(event.type)) {
12128
+ const message = [data.message, data.error?.message].find((v) => typeof v === "string");
12129
+ if (message)
12130
+ lastError = message;
12131
+ }
10846
12132
  }
10847
- if (end === -1)
10848
- break;
10849
- const candidate = tryParseObject(s.slice(i, end + 1));
10850
- if (candidate)
10851
- last = candidate;
10852
- i = end + 1;
10853
12133
  }
10854
- return last;
12134
+ return { finalText, readPaths, toolInputs, failure: finalText === null ? lastError : null };
10855
12135
  }
10856
- function tryParseObject(s) {
10857
- try {
10858
- const parsed = JSON.parse(s);
10859
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
10860
- } catch {
10861
- return void 0;
12136
+ function parseCopilotModels(helpText) {
12137
+ const lines = helpText.split("\n");
12138
+ const start = lines.findIndex((line) => /^\s*`model`:/.test(line));
12139
+ if (start === -1)
12140
+ return [];
12141
+ const models = [];
12142
+ for (const line of lines.slice(start + 1)) {
12143
+ const value = /^\s+-\s+"([^"]+)"\s*$/.exec(line);
12144
+ if (!value)
12145
+ break;
12146
+ models.push({ id: value[1], label: value[1], efforts: [...models_1.REASONING_EFFORTS] });
10862
12147
  }
12148
+ return models;
12149
+ }
12150
+ function createCopilotExecutor(config, capabilityId = "copilot") {
12151
+ const validated = (0, agent_common_1.parseAgentCommonConfig)(config, capabilityId);
12152
+ const effort = (0, models_1.parseReasoningEffort)(config.reasoningEffort, models_1.REASONING_EFFORTS);
12153
+ if (effort)
12154
+ validated.reasoningEffort = effort;
12155
+ return new CopilotExecutor(validated);
10863
12156
  }
10864
12157
  }
10865
12158
  });
@@ -10889,6 +12182,14 @@ var require_http_api = __commonJS({
10889
12182
  outputTokens: spec.costCapHint?.estimatedOutputTokens
10890
12183
  };
10891
12184
  }
12185
+ /**
12186
+ * The one model it runs. This executor takes no model hint (every call
12187
+ * names the configured model), so reporting the endpoint's catalog would
12188
+ * offer picks it would silently ignore.
12189
+ */
12190
+ async listModels() {
12191
+ return [{ id: this.config.model, label: this.config.model }];
12192
+ }
10892
12193
  async execute(dispatch, signal) {
10893
12194
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
10894
12195
  const outputLabel = spec.outputLabel;
@@ -11829,10 +13130,12 @@ var require_anthropic_api = __commonJS({
11829
13130
  "use strict";
11830
13131
  Object.defineProperty(exports2, "__esModule", { value: true });
11831
13132
  exports2.createAnthropicApiExecutor = createAnthropicApiExecutor;
13133
+ exports2.parseAnthropicModels = parseAnthropicModels;
11832
13134
  var loom_1 = require_dist();
11833
13135
  var keep_1 = require_dist2();
11834
13136
  var env_ref_1 = require_env_ref();
11835
13137
  var validate_output_1 = require_validate_output();
13138
+ var models_1 = require_models();
11836
13139
  var DEFAULT_BASE_URL = "https://api.anthropic.com/v1/messages";
11837
13140
  var DEFAULT_MODEL = "claude-sonnet-4-6";
11838
13141
  var DEFAULT_MAX_TOKENS = 4096;
@@ -11888,6 +13191,37 @@ var require_anthropic_api = __commonJS({
11888
13191
  };
11889
13192
  return this.callApi(handle.apiKey, body, spec, handle.outputLabel, signal);
11890
13193
  }
13194
+ /**
13195
+ * The provider's own catalog, `/v1/models` beside the configured Messages
13196
+ * URL, asked with the operator's key. Dynamic mode reports nothing: its key
13197
+ * is a customer's, revealed per job, and there is none to ask with at start.
13198
+ */
13199
+ async listModels(signal) {
13200
+ const apiKey = this.instance.staticKey;
13201
+ if (!apiKey)
13202
+ return [];
13203
+ const url = modelsUrl(this.instance.baseUrl);
13204
+ if (!url)
13205
+ return [];
13206
+ const controller = new AbortController();
13207
+ const abort = () => controller.abort();
13208
+ const timer = setTimeout(abort, models_1.LIST_MODELS_TIMEOUT_MS);
13209
+ signal.addEventListener("abort", abort, { once: true });
13210
+ try {
13211
+ const response = await fetch(url, {
13212
+ headers: { "x-api-key": apiKey, "anthropic-version": ANTHROPIC_API_VERSION },
13213
+ signal: controller.signal
13214
+ });
13215
+ if (!response.ok)
13216
+ return [];
13217
+ return parseAnthropicModels(await response.json());
13218
+ } catch {
13219
+ return [];
13220
+ } finally {
13221
+ clearTimeout(timer);
13222
+ signal.removeEventListener("abort", abort);
13223
+ }
13224
+ }
11891
13225
  /** Shared Messages-API call + parse + validate, used by execute and continue. */
11892
13226
  async callApi(apiKey, body, spec, outputLabel, signal) {
11893
13227
  const timeoutController = new AbortController();
@@ -11929,7 +13263,8 @@ var require_anthropic_api = __commonJS({
11929
13263
  inputTokens: u.input_tokens,
11930
13264
  outputTokens: u.output_tokens,
11931
13265
  aiProvider: "anthropic",
11932
- aiModel: this.instance.model
13266
+ // The model this call ran, which a hint may have chosen.
13267
+ aiModel: body.model
11933
13268
  } : void 0;
11934
13269
  const parsed = safeJsonParse(rawText);
11935
13270
  (0, validate_output_1.assertOutputValid)(spec, parsed);
@@ -11961,7 +13296,8 @@ var require_anthropic_api = __commonJS({
11961
13296
  if (rawApiKeyFrom !== void 0 && rawApiKey !== void 0) {
11962
13297
  throw new Error('AnthropicApiExecutor accepts either "apiKey" (static) or "apiKeyFrom" (dynamic), not both');
11963
13298
  }
11964
- const resolveKey = rawApiKeyFrom !== void 0 ? buildDynamicResolver(config, deps) : buildStaticResolver(rawApiKey);
13299
+ const staticKey = rawApiKeyFrom === void 0 ? resolveStaticKey(rawApiKey) : null;
13300
+ const resolveKey = staticKey === null ? buildDynamicResolver(config, deps) : async () => staticKey;
11965
13301
  let baseUrl = DEFAULT_BASE_URL;
11966
13302
  if (config.baseUrl !== void 0) {
11967
13303
  if (typeof config.baseUrl !== "string" || config.baseUrl.length === 0) {
@@ -11992,6 +13328,7 @@ var require_anthropic_api = __commonJS({
11992
13328
  }
11993
13329
  return new AnthropicApiExecutor({
11994
13330
  resolveKey,
13331
+ staticKey,
11995
13332
  baseUrl,
11996
13333
  model,
11997
13334
  maxTokens,
@@ -11999,12 +13336,25 @@ var require_anthropic_api = __commonJS({
11999
13336
  capabilityId
12000
13337
  });
12001
13338
  }
12002
- function buildStaticResolver(rawApiKey) {
13339
+ function resolveStaticKey(rawApiKey) {
12003
13340
  if (typeof rawApiKey !== "string" || rawApiKey.length === 0) {
12004
13341
  throw new Error('AnthropicApiExecutor requires a non-empty "apiKey" string in config (or "apiKeyFrom: \\"job-metadata\\"" for dynamic key mode)');
12005
13342
  }
12006
- const resolved = (0, env_ref_1.resolveEnvRef)(rawApiKey, "apiKey");
12007
- return async () => resolved;
13343
+ return (0, env_ref_1.resolveEnvRef)(rawApiKey, "apiKey");
13344
+ }
13345
+ function modelsUrl(messagesUrl) {
13346
+ return /\/messages\/?$/.test(messagesUrl) ? messagesUrl.replace(/\/messages\/?$/, "/models") : null;
13347
+ }
13348
+ function parseAnthropicModels(body) {
13349
+ const data = body?.data;
13350
+ if (!Array.isArray(data))
13351
+ return [];
13352
+ return data.flatMap((entry) => {
13353
+ const { id, display_name: displayName } = entry ?? {};
13354
+ if (typeof id !== "string" || id.length === 0)
13355
+ return [];
13356
+ return [{ id, label: typeof displayName === "string" && displayName ? displayName : id }];
13357
+ });
12008
13358
  }
12009
13359
  function buildDynamicResolver(config, deps) {
12010
13360
  if (config.apiKeyFrom !== "job-metadata") {
@@ -12502,6 +13852,8 @@ var require_checks = __commonJS({
12502
13852
  var path_1 = require("path");
12503
13853
  var registry_1 = require_registry();
12504
13854
  var claude_code_1 = require_claude_code();
13855
+ var codex_1 = require_codex();
13856
+ var copilot_1 = require_copilot();
12505
13857
  var http_api_1 = require_http_api();
12506
13858
  var anthropic_api_1 = require_anthropic_api();
12507
13859
  var webhook_1 = require_webhook();
@@ -12513,9 +13865,21 @@ var require_checks = __commonJS({
12513
13865
  var scaffold_1 = require_scaffold();
12514
13866
  var NODE_FLOOR = "20.0.0";
12515
13867
  var DISK_FLOOR_BYTES = 1024 * 1024 * 1024;
12516
- var WORKSPACE_EXECUTOR = "claude-code";
13868
+ var AGENTS = [
13869
+ { executor: "claude-code", bin: "claude", install: "install Claude Code (npm install -g @anthropic-ai/claude-code)" },
13870
+ { executor: "codex", bin: "codex", install: "install Codex (brew install codex, or npm install -g @openai/codex)" },
13871
+ {
13872
+ executor: "copilot",
13873
+ bin: "copilot",
13874
+ install: "install GitHub Copilot CLI (brew install --cask copilot-cli, or npm install -g @github/copilot)"
13875
+ }
13876
+ ];
13877
+ var AGENT_EXECUTORS2 = AGENTS.map((a) => a.executor);
13878
+ var DEFAULT_AGENT = "claude-code";
12517
13879
  var BUILT_IN_EXECUTORS = [
12518
13880
  ["claude-code", claude_code_1.createClaudeCodeExecutor],
13881
+ ["codex", codex_1.createCodexExecutor],
13882
+ ["copilot", copilot_1.createCopilotExecutor],
12519
13883
  ["http-api", http_api_1.createHttpApiExecutor],
12520
13884
  ["anthropic-api", anthropic_api_1.createAnthropicApiExecutor],
12521
13885
  ["webhook", webhook_1.createWebhookExecutor],
@@ -12544,22 +13908,27 @@ var require_checks = __commonJS({
12544
13908
  function configuredExecutors(config) {
12545
13909
  return Object.keys(config.executors ?? {});
12546
13910
  }
12547
- function claudeCodeSettings(config, subject) {
12548
- const user = config.executors[WORKSPACE_EXECUTOR];
12549
- if (user === void 0)
13911
+ function activeAgent(config) {
13912
+ const configured = configuredExecutors(config);
13913
+ const pooled = config.pools.map((p) => p.executor).find((e) => AGENT_EXECUTORS2.includes(e) && configured.includes(e));
13914
+ return pooled ?? configured.find((e) => AGENT_EXECUTORS2.includes(e)) ?? null;
13915
+ }
13916
+ function agentSettings(config, subject) {
13917
+ const executor = activeAgent(config);
13918
+ if (executor === null)
12550
13919
  return null;
12551
- const defaults = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[WORKSPACE_EXECUTOR] ?? {};
12552
- return { ...defaults, ...user };
13920
+ const user = config.executors[executor] ?? {};
13921
+ const defaults = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[executor] ?? {};
13922
+ return { executor, settings: { ...defaults, ...user } };
12553
13923
  }
12554
13924
  function claudeSettingsOf(config) {
12555
- return config.executors[WORKSPACE_EXECUTOR]?.claudeSettings;
13925
+ return config.executors["claude-code"]?.claudeSettings;
12556
13926
  }
12557
13927
  function describeLogin(status) {
12558
13928
  return [status.authMethod ?? "unknown method", status.subscriptionType].filter((part) => part !== null).join(", ");
12559
13929
  }
12560
13930
  function runRoot(config, subject, osTmpDir) {
12561
- const settings = claudeCodeSettings(config, subject);
12562
- const tmpDir = settings?.tmpDir;
13931
+ const tmpDir = agentSettings(config, subject)?.settings.tmpDir;
12563
13932
  return typeof tmpDir === "string" && tmpDir.length > 0 ? (0, apply_1.expandHome)(tmpDir) : osTmpDir;
12564
13933
  }
12565
13934
  async function withConfig(name, probes, body) {
@@ -12585,13 +13954,13 @@ var require_checks = __commonJS({
12585
13954
  };
12586
13955
  }
12587
13956
  function initRunRoot(plan, subject, osTmpDir) {
12588
- if (plan.executor !== WORKSPACE_EXECUTOR)
13957
+ if (plan.executor === void 0 || !AGENT_EXECUTORS2.includes(plan.executor))
12589
13958
  return osTmpDir;
12590
13959
  if (subject.requestedTmpDir !== void 0 && subject.requestedTmpDir.length > 0) {
12591
13960
  return (0, apply_1.expandHome)(subject.requestedTmpDir);
12592
13961
  }
12593
13962
  const settings = {
12594
- ...(0, environment_1.executorDefaultsFor)(subject.brand, subject.brand.keepRegistration.jobsApiUrl)?.[WORKSPACE_EXECUTOR] ?? {},
13963
+ ...(0, environment_1.executorDefaultsFor)(subject.brand, subject.brand.keepRegistration.jobsApiUrl)?.[plan.executor] ?? {},
12595
13964
  ...plan.keptBlock ?? {}
12596
13965
  };
12597
13966
  const tmpDir = settings.tmpDir;
@@ -12787,7 +14156,7 @@ var require_checks = __commonJS({
12787
14156
  }
12788
14157
  const covering = built.filter((e) => e.strategies.includes(loom_1.PROMPT_EXECUTION_STRATEGY));
12789
14158
  if (covering.length === 0) {
12790
- return fail("executor-coverage", `no configured executor handles the "${loom_1.PROMPT_EXECUTION_STRATEGY}" strategy every review job carries`, `configure an executor that runs prompts, e.g. ${WORKSPACE_EXECUTOR}`);
14159
+ return fail("executor-coverage", `no configured executor handles the "${loom_1.PROMPT_EXECUTION_STRATEGY}" strategy every review job carries`, `configure an executor that runs prompts, e.g. ${DEFAULT_AGENT}`);
12791
14160
  }
12792
14161
  return pass("executor-coverage", `${covering.map((e) => e.capability.id).join(", ")} handle${covering.length === 1 ? "s" : ""} ${loom_1.PROMPT_EXECUTION_STRATEGY}`);
12793
14162
  });
@@ -12798,13 +14167,13 @@ var require_checks = __commonJS({
12798
14167
  phases: ["init", "run", "doctor"],
12799
14168
  async run(probes, subject) {
12800
14169
  if (subject.phase === "init") {
12801
- return atInit("git-cli", probes, subject, () => toolVerdict("git-cli", "git", probes, subject, {
14170
+ return atInit("git-cli", AGENT_EXECUTORS2, probes, subject, () => toolVerdict("git-cli", "git", probes, subject, {
12802
14171
  missingRemedy: "install git, or configure an executor that needs no checkout"
12803
14172
  }));
12804
14173
  }
12805
14174
  return withConfig("git-cli", probes, async (config) => {
12806
- if (!configuredExecutors(config).includes(WORKSPACE_EXECUTOR)) {
12807
- return skip("git-cli", `no ${WORKSPACE_EXECUTOR} executor configured; nothing checks out a workspace`);
14175
+ if (activeAgent(config) === null) {
14176
+ return skip("git-cli", "no agent executor configured; nothing checks out a workspace");
12808
14177
  }
12809
14178
  return toolVerdict("git-cli", "git", probes, subject, {
12810
14179
  missingRemedy: "install git, or configure an executor that needs no checkout"
@@ -12812,28 +14181,32 @@ var require_checks = __commonJS({
12812
14181
  });
12813
14182
  }
12814
14183
  };
12815
- var claudeCli = {
12816
- name: "claude-cli",
12817
- phases: ["init", "run", "doctor"],
12818
- async run(probes, subject) {
12819
- if (subject.phase === "init") {
12820
- return atInit("claude-cli", probes, subject, () => toolVerdict("claude-cli", "claude", probes, subject, { missingRemedy: CLAUDE_INSTALL }));
12821
- }
12822
- return withConfig("claude-cli", probes, async (config) => {
12823
- if (!configuredExecutors(config).includes(WORKSPACE_EXECUTOR)) {
12824
- return skip("claude-cli", `no ${WORKSPACE_EXECUTOR} executor configured`);
14184
+ function agentCli(agent) {
14185
+ const name = `${agent.bin}-cli`;
14186
+ return {
14187
+ name,
14188
+ phases: ["init", "run", "doctor"],
14189
+ async run(probes, subject) {
14190
+ if (subject.phase === "init") {
14191
+ return atInit(name, [agent.executor], probes, subject, () => toolVerdict(name, agent.bin, probes, subject, { missingRemedy: agent.install }));
12825
14192
  }
12826
- return toolVerdict("claude-cli", "claude", probes, subject, { missingRemedy: CLAUDE_INSTALL });
12827
- });
12828
- }
12829
- };
14193
+ return withConfig(name, probes, async (config) => {
14194
+ if (!configuredExecutors(config).includes(agent.executor)) {
14195
+ return skip(name, `no ${agent.executor} executor configured`);
14196
+ }
14197
+ return toolVerdict(name, agent.bin, probes, subject, { missingRemedy: agent.install });
14198
+ });
14199
+ }
14200
+ };
14201
+ }
14202
+ var [CLAUDE, CODEX, COPILOT] = AGENTS;
12830
14203
  var claudeAuth = {
12831
14204
  name: "claude-auth",
12832
14205
  phases: ["run", "doctor"],
12833
14206
  async run(probes, subject) {
12834
14207
  return withConfig("claude-auth", probes, async (config) => {
12835
- if (!configuredExecutors(config).includes(WORKSPACE_EXECUTOR)) {
12836
- return skip("claude-auth", `no ${WORKSPACE_EXECUTOR} executor configured`);
14208
+ if (!configuredExecutors(config).includes(CLAUDE.executor)) {
14209
+ return skip("claude-auth", `no ${CLAUDE.executor} executor configured`);
12837
14210
  }
12838
14211
  if (config.credentials !== void 0) {
12839
14212
  return skip("claude-auth", "hosted mode: the model key is revealed per job, not read from a login");
@@ -12867,6 +14240,64 @@ var require_checks = __commonJS({
12867
14240
  });
12868
14241
  }
12869
14242
  };
14243
+ var codexAuth = {
14244
+ name: "codex-auth",
14245
+ phases: ["run", "doctor"],
14246
+ async run(probes, subject) {
14247
+ return withConfig("codex-auth", probes, async (config) => {
14248
+ const gate = await agentAuthGate("codex-auth", CODEX, config, probes);
14249
+ if (gate)
14250
+ return gate;
14251
+ if (subject.deep)
14252
+ return roundTripVerdict("codex-auth", CODEX, probes, "run `codex login`");
14253
+ const status = await probes.codexLoginStatus();
14254
+ if (status === null) {
14255
+ return warn("codex-auth", "`codex login status` gave no answer", "run `codex login status` to see why, or doctor --deep");
14256
+ }
14257
+ if (!status.loggedIn) {
14258
+ return fail("codex-auth", "the codex CLI is not signed in", "run `codex login`");
14259
+ }
14260
+ return pass("codex-auth", `${status.detail} (not verified)`);
14261
+ });
14262
+ }
14263
+ };
14264
+ var copilotAuth = {
14265
+ name: "copilot-auth",
14266
+ phases: ["run", "doctor"],
14267
+ async run(probes, subject) {
14268
+ return withConfig("copilot-auth", probes, async (config) => {
14269
+ const gate = await agentAuthGate("copilot-auth", COPILOT, config, probes);
14270
+ if (gate)
14271
+ return gate;
14272
+ if (subject.deep)
14273
+ return roundTripVerdict("copilot-auth", COPILOT, probes, "run `copilot login`");
14274
+ const token = probes.ambientCopilotToken();
14275
+ if (token !== null) {
14276
+ return warn("copilot-auth", `${token} is set and Copilot uses it ahead of any \`copilot login\``, `unset ${token} for this process if it is not a token with Copilot access, or run doctor --deep to prove it`);
14277
+ }
14278
+ return pass("copilot-auth", "no token in the environment; the copilot login is used (not verified; doctor --deep spends one turn to prove it)");
14279
+ });
14280
+ }
14281
+ };
14282
+ async function agentAuthGate(name, agent, config, probes) {
14283
+ if (!configuredExecutors(config).includes(agent.executor)) {
14284
+ return skip(name, `no ${agent.executor} executor configured`);
14285
+ }
14286
+ if (config.credentials !== void 0) {
14287
+ return skip(name, `hosted mode: the ${agent.executor} executor refuses hosted runs`);
14288
+ }
14289
+ if (await probes.which(agent.bin) === null || !(await probes.versionOf(agent.bin)).ran) {
14290
+ return skip(name, `no working ${agent.bin} CLI to be signed in to; see ${agent.bin}-cli`);
14291
+ }
14292
+ return null;
14293
+ }
14294
+ async function roundTripVerdict(name, agent, probes, signIn) {
14295
+ const round = await probes.agentRoundTrip(agent.executor);
14296
+ if (!round.ok) {
14297
+ return fail(name, `the ${agent.bin} CLI refused a one-turn call: ${oneLine(round.error)}`, `${signIn}, then re-run doctor --deep`);
14298
+ }
14299
+ return pass(name, `one-turn call through the ${agent.bin} CLI succeeded`);
14300
+ }
12870
14301
  var runRootWritable = {
12871
14302
  name: "run-root-writable",
12872
14303
  phases: ["init", "run", "doctor"],
@@ -12930,15 +14361,15 @@ var require_checks = __commonJS({
12930
14361
  phases: ["run", "doctor"],
12931
14362
  async run(probes, subject) {
12932
14363
  return withConfig("overlay-hosts", probes, (config) => {
12933
- const settings = claudeCodeSettings(config, subject);
12934
- if (settings === null) {
12935
- return skip("overlay-hosts", `no ${WORKSPACE_EXECUTOR} executor configured`);
14364
+ const agent = agentSettings(config, subject);
14365
+ if (agent === null) {
14366
+ return skip("overlay-hosts", "no agent executor configured");
12936
14367
  }
12937
- const hosts = settings.overlayAllowedHosts;
14368
+ const hosts = agent.settings.overlayAllowedHosts;
12938
14369
  const origins = Array.isArray(hosts) ? hosts.filter((h) => typeof h === "string") : [];
12939
14370
  if (origins.length === 0) {
12940
- const fallback = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[WORKSPACE_EXECUTOR]?.overlayAllowedHosts;
12941
- return warn("overlay-hosts", "the overlay allowlist is empty, so every review will run standards-blind", Array.isArray(fallback) && fallback.length > 0 ? `remove overlayAllowedHosts from the ${WORKSPACE_EXECUTOR} executor to use this tier's default` : `add overlayAllowedHosts to the ${WORKSPACE_EXECUTOR} executor, naming the origin your backend signs standards URLs with (this build names none for Jobs at ${(0, environment_1.hostOf)(config.auth.jobsApiUrl)})`);
14371
+ const fallback = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[agent.executor]?.overlayAllowedHosts;
14372
+ return warn("overlay-hosts", "the overlay allowlist is empty, so every review will run standards-blind", Array.isArray(fallback) && fallback.length > 0 ? `remove overlayAllowedHosts from the ${agent.executor} executor to use this tier's default` : `add overlayAllowedHosts to the ${agent.executor} executor, naming the origin your backend signs standards URLs with (this build names none for Jobs at ${(0, environment_1.hostOf)(config.auth.jobsApiUrl)})`);
12942
14373
  }
12943
14374
  return pass("overlay-hosts", origins.join(", "));
12944
14375
  });
@@ -12969,8 +14400,12 @@ var require_checks = __commonJS({
12969
14400
  needsFetch(keepReachable),
12970
14401
  executorCoverage,
12971
14402
  gitCli,
12972
- claudeCli,
14403
+ agentCli(CLAUDE),
12973
14404
  claudeAuth,
14405
+ agentCli(CODEX),
14406
+ codexAuth,
14407
+ agentCli(COPILOT),
14408
+ copilotAuth,
12974
14409
  runRootWritable,
12975
14410
  diskSpace,
12976
14411
  overlayHosts,
@@ -13002,13 +14437,12 @@ var require_checks = __commonJS({
13002
14437
  }
13003
14438
  return `run \`${bin} --version\` to see why ${found} fails, then reinstall it (${missingRemedy})`;
13004
14439
  }
13005
- var CLAUDE_INSTALL = "install Claude Code (npm install -g @anthropic-ai/claude-code)";
13006
- async function atInit(name, probes, subject, body) {
14440
+ async function atInit(name, needs, probes, subject, body) {
13007
14441
  const plan = initPlan(probes, subject);
13008
14442
  if (!plan.ok)
13009
14443
  return skip(name, "the existing config could not be read; see executor-coverage");
13010
- if (plan.value.executor !== WORKSPACE_EXECUTOR) {
13011
- return skip(name, plan.value.executor === void 0 ? "init configures no executor for this build" : `init configures ${plan.value.executor}, which needs no ${WORKSPACE_EXECUTOR} tools`);
14444
+ if (plan.value.executor === void 0 || !needs.includes(plan.value.executor)) {
14445
+ return skip(name, plan.value.executor === void 0 ? "init configures no executor for this build" : `init configures ${plan.value.executor}, which does not need this tool`);
13012
14446
  }
13013
14447
  return body();
13014
14448
  }
@@ -20914,12 +22348,14 @@ var require_subscribe = __commonJS({
20914
22348
  if (id === STOPPED)
20915
22349
  return;
20916
22350
  resolvedPoolIds.push(id);
22351
+ const perPool = opts.subscriber.poolMetadata?.(ref);
22352
+ const metadata = opts.subscriber.metadata !== void 0 || perPool !== void 0 ? { ...opts.subscriber.metadata, ...perPool } : void 0;
20917
22353
  const registered = await untilAvailable(() => opts.jobsClient.registerMember(id, {
20918
22354
  identityType: opts.subscriber.identityType,
20919
22355
  identityId: opts.subscriber.identityId,
20920
22356
  intelligence: opts.subscriber.intelligence,
20921
22357
  ...opts.subscriber.displayName !== void 0 ? { displayName: opts.subscriber.displayName } : {},
20922
- ...opts.subscriber.metadata !== void 0 ? { metadata: opts.subscriber.metadata } : {}
22358
+ ...metadata !== void 0 ? { metadata } : {}
20923
22359
  }), mySession);
20924
22360
  if (registered === STOPPED)
20925
22361
  return;
@@ -21233,7 +22669,7 @@ var require_schema4 = __commonJS({
21233
22669
  message: 'Each pool must specify at least one of "id" or "name"'
21234
22670
  });
21235
22671
  var executorSchema = zod_1.z.object({
21236
- type: zod_1.z.enum(["claude-code", "http-api", "anthropic-api", "webhook", "custom-script"])
22672
+ type: zod_1.z.enum(["claude-code", "codex", "copilot", "http-api", "anthropic-api", "webhook", "custom-script"])
21237
22673
  }).passthrough();
21238
22674
  var shuttleConfigSchema = zod_1.z.object({
21239
22675
  auth: zod_1.z.object({
@@ -21483,11 +22919,14 @@ var require_probes = __commonJS({
21483
22919
  var jobs_1 = require_dist4();
21484
22920
  var scaffold_1 = require_scaffold();
21485
22921
  var loader_1 = require_loader();
22922
+ var codex_1 = require_codex();
22923
+ var copilot_1 = require_copilot();
21486
22924
  var store_1 = require_store();
21487
22925
  var version_check_1 = require_version_check();
21488
22926
  var PROBE_TIMEOUT_MS = 1e4;
21489
22927
  var VERSION_TIMEOUT_MS = 1e4;
21490
22928
  var DEEP_PROBE_PROMPT = "Reply with the single word: ok";
22929
+ var COPILOT_TOKEN_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
21491
22930
  function createProbes(options) {
21492
22931
  let observedLatest = null;
21493
22932
  const observe = (response) => {
@@ -21708,6 +23147,24 @@ var require_probes = __commonJS({
21708
23147
  }
21709
23148
  return { ok: true, value: void 0 };
21710
23149
  },
23150
+ async codexLoginStatus() {
23151
+ const outcome = await runCapturing("codex", ["login", "status"], VERSION_TIMEOUT_MS);
23152
+ if (outcome.ok) {
23153
+ const line = `${outcome.stdout}
23154
+ ${outcome.stderr}`.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
23155
+ return { loggedIn: true, detail: line ?? "signed in" };
23156
+ }
23157
+ if (outcome.exited && /not logged in/i.test(outcome.reason)) {
23158
+ return { loggedIn: false, detail: "not logged in" };
23159
+ }
23160
+ return null;
23161
+ },
23162
+ ambientCopilotToken: () => COPILOT_TOKEN_VARS.find((name) => (process.env[name] ?? "").length > 0) ?? null,
23163
+ async agentRoundTrip(agent) {
23164
+ const executor = agent === "codex" ? (0, codex_1.createCodexExecutor)({}) : (0, copilot_1.createCopilotExecutor)({});
23165
+ const readiness = await executor.checkReadiness(AbortSignal.timeout(18e4));
23166
+ return readiness.ready ? { ok: true, value: void 0 } : { ok: false, error: readiness.reason };
23167
+ },
21711
23168
  latestVersion: () => observedLatest
21712
23169
  };
21713
23170
  }
@@ -21774,7 +23231,7 @@ var require_probes = __commonJS({
21774
23231
  child.on("close", (code, signal) => {
21775
23232
  clearTimeout(timer);
21776
23233
  if (code === 0) {
21777
- settle({ ok: true, stdout: stdout.trim() });
23234
+ settle({ ok: true, stdout: stdout.trim(), stderr: stderr.trim() });
21778
23235
  return;
21779
23236
  }
21780
23237
  const status = code === null ? `was killed by ${signal ?? "a signal"}` : `exited ${code}`;
@@ -25380,6 +26837,37 @@ var require_logger = __commonJS({
25380
26837
  }
25381
26838
  });
25382
26839
 
26840
+ // ../../packages/shuttle/dist/core/demux.js
26841
+ var require_demux = __commonJS({
26842
+ "../../packages/shuttle/dist/core/demux.js"(exports2) {
26843
+ "use strict";
26844
+ Object.defineProperty(exports2, "__esModule", { value: true });
26845
+ exports2.createDemux = createDemux;
26846
+ function createDemux(executors, pools) {
26847
+ const byStrategy = /* @__PURE__ */ new Map();
26848
+ for (const executor of executors) {
26849
+ for (const strategy of executor.strategies)
26850
+ byStrategy.set(strategy, executor);
26851
+ }
26852
+ const byName = new Map(executors.map((e) => [e.capability.id, e]));
26853
+ const byPool = /* @__PURE__ */ new Map();
26854
+ for (const pool of pools) {
26855
+ const named = byName.get(pool.executor);
26856
+ if (!named)
26857
+ continue;
26858
+ if (pool.id)
26859
+ byPool.set(`id:${pool.id}`, named);
26860
+ if (pool.name)
26861
+ byPool.set(`name:${pool.name}`, named);
26862
+ }
26863
+ return (pool, strategy) => {
26864
+ const named = byPool.get(`id:${pool.id}`) ?? byPool.get(`name:${pool.name}`);
26865
+ return named && named.strategies.includes(strategy) ? named : byStrategy.get(strategy);
26866
+ };
26867
+ }
26868
+ }
26869
+ });
26870
+
25383
26871
  // ../../packages/shuttle/dist/observers/logger.js
25384
26872
  var require_logger2 = __commonJS({
25385
26873
  "../../packages/shuttle/dist/observers/logger.js"(exports2) {
@@ -25901,6 +27389,9 @@ var require_shuttle = __commonJS({
25901
27389
  var logger_1 = require_logger();
25902
27390
  var registry_1 = require_registry();
25903
27391
  var claude_code_1 = require_claude_code();
27392
+ var demux_1 = require_demux();
27393
+ var codex_1 = require_codex();
27394
+ var copilot_1 = require_copilot();
25904
27395
  var http_api_1 = require_http_api();
25905
27396
  var anthropic_api_1 = require_anthropic_api();
25906
27397
  var webhook_1 = require_webhook();
@@ -25951,6 +27442,13 @@ var require_shuttle = __commonJS({
25951
27442
  });
25952
27443
  return { promise, resolve, reject };
25953
27444
  }
27445
+ async function listExecutorModels(executors, signal) {
27446
+ const listed = await Promise.all(executors.map(async (executor) => {
27447
+ const models = executor.listModels ? await executor.listModels(signal).catch(() => []) : [];
27448
+ return [executor.capability.id, models];
27449
+ }));
27450
+ return new Map(listed);
27451
+ }
25954
27452
  function isKeepUnavailable(err) {
25955
27453
  return err instanceof Error && err.name === "KeepUnavailableError";
25956
27454
  }
@@ -26098,11 +27596,11 @@ var require_shuttle = __commonJS({
26098
27596
  const registry = new registry_1.ExecutorRegistry(this.brand.executorAllowlist);
26099
27597
  registerBuiltInExecutors(registry, this.brand.executorAllowlist);
26100
27598
  const executors = registry.build(mergeExecutorDefaults((0, environment_1.executorDefaultsFor)(this.brand, this.config.auth.jobsApiUrl), this.config.executors));
26101
- const byStrategy = /* @__PURE__ */ new Map();
26102
- for (const executor of executors) {
26103
- for (const strategy of executor.strategies)
26104
- byStrategy.set(strategy, executor);
26105
- }
27599
+ const executorFor = (0, demux_1.createDemux)(executors, this.config.pools);
27600
+ const modelsByExecutor = await listExecutorModels(executors, this.stopping.signal);
27601
+ if (this.stopping.signal.aborted)
27602
+ return;
27603
+ logger.info("executor models listed", Object.fromEntries([...modelsByExecutor].map(([id, m]) => [id, m.length])));
26106
27604
  this.spendTracker = (0, spend_tracker_1.createSpendTrackerObserver)(this.brand, paths.spendStateFile);
26107
27605
  const observers = [
26108
27606
  (0, logger_2.createLoggerObserver)(),
@@ -26123,7 +27621,7 @@ var require_shuttle = __commonJS({
26123
27621
  const attendanceId = dispatch.attendanceId;
26124
27622
  const startedAt = Date.now();
26125
27623
  const strategy = (0, loom_1.readShuttleStrategy)(dispatch.inputs) ?? "";
26126
- const executor = byStrategy.get(strategy);
27624
+ const executor = executorFor(dispatch.pool, strategy);
26127
27625
  if (!executor) {
26128
27626
  const reason = `no executor for shuttle strategy "${strategy}"`;
26129
27627
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
@@ -26218,6 +27716,14 @@ var require_shuttle = __commonJS({
26218
27716
  return { status: "completed", outputs };
26219
27717
  };
26220
27718
  const pools = this.config.pools.map((p) => p.id ? { id: p.id } : { name: p.name });
27719
+ const poolExecutors = new Map(pools.map((ref, i) => [ref, this.config.pools[i].executor]));
27720
+ const poolMetadata = (ref) => {
27721
+ const executor = poolExecutors.get(ref);
27722
+ const models = executor ? modelsByExecutor.get(executor) : void 0;
27723
+ if (!executor || !models)
27724
+ return void 0;
27725
+ return { executor, ...models.length > 0 ? { models } : {} };
27726
+ };
26221
27727
  const attemptedUpgrades = /* @__PURE__ */ new Set();
26222
27728
  this.subscription = (0, jobs_1.subscribe)({
26223
27729
  jobsClient,
@@ -26234,7 +27740,8 @@ var require_shuttle = __commonJS({
26234
27740
  ...this.config.agent.metadata,
26235
27741
  client_version: this.brand.product.version,
26236
27742
  hostname: (0, os_1.hostname)()
26237
- }
27743
+ },
27744
+ poolMetadata
26238
27745
  },
26239
27746
  onJob,
26240
27747
  onDirectives: async (directives) => {
@@ -26378,6 +27885,8 @@ var require_shuttle = __commonJS({
26378
27885
  function registerBuiltInExecutors(registry, allowlist) {
26379
27886
  const all = [
26380
27887
  ["claude-code", claude_code_1.createClaudeCodeExecutor],
27888
+ ["codex", codex_1.createCodexExecutor],
27889
+ ["copilot", copilot_1.createCopilotExecutor],
26381
27890
  ["http-api", http_api_1.createHttpApiExecutor],
26382
27891
  ["anthropic-api", anthropic_api_1.createAnthropicApiExecutor],
26383
27892
  ["webhook", webhook_1.createWebhookExecutor],
@@ -26880,8 +28389,8 @@ var require_dist5 = __commonJS({
26880
28389
  "../../packages/shuttle/dist/index.js"(exports2) {
26881
28390
  "use strict";
26882
28391
  Object.defineProperty(exports2, "__esModule", { value: true });
26883
- exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.executorDefaultsFor = exports2.environmentReportJson = exports2.environmentPolledBy = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorEnvironmentError = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
26884
- exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.scaffoldedConfigPath = exports2.planScaffold = void 0;
28392
+ exports2.DEFAULT_DAILY_PERIOD = exports2.SUBSTRATE_BRAND = exports2.verificationJson = exports2.resolveEnvironment = exports2.renderVerificationLine = exports2.renderEnvironmentReport = exports2.isWorthReporting = exports2.hostOf = exports2.executorDefaultsFor = exports2.environmentReportJson = exports2.environmentPolledBy = exports2.applyEnvironment = exports2.renderTemplate = exports2.expandHome = exports2.resolvePaths = exports2.SpendStore = exports2.AuditLogStore = exports2.createSpendTrackerObserver = exports2.createAuditLogObserver = exports2.createLoggerObserver = exports2.SpendCapExceeded = exports2.ObserverChain = exports2.createCustomScriptExecutor = exports2.createWebhookExecutor = exports2.createHttpApiExecutor = exports2.createCopilotExecutor = exports2.createCodexExecutor = exports2.createClaudeCodeExecutor = exports2.ExecutorEnvironmentError = exports2.ExecutorRegistry = exports2.AgentCredentialRevokedError = exports2.runPreflight = exports2.renderLines = exports2.renderJson = exports2.printReport = exports2.hasFailure = exports2.createProbes = exports2.checksFor = exports2.PREFLIGHT_FLOORS = exports2.PREFLIGHT_EXIT_CODE = exports2.CHECKS = exports2.readResponseLatestVersion = exports2.checkResponseMinVersion = exports2.compareSemver = exports2.UpgradeRequiredError = exports2.LATEST_VERSION_HEADER = exports2.MIN_VERSION_HEADER = exports2.loadConfig = exports2.Shuttle = exports2.createCli = void 0;
28393
+ exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.scaffoldedConfigPath = exports2.planScaffold = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = void 0;
26885
28394
  var cli_1 = require_cli();
26886
28395
  Object.defineProperty(exports2, "createCli", { enumerable: true, get: function() {
26887
28396
  return cli_1.createCli;
@@ -26960,6 +28469,14 @@ var require_dist5 = __commonJS({
26960
28469
  Object.defineProperty(exports2, "createClaudeCodeExecutor", { enumerable: true, get: function() {
26961
28470
  return claude_code_1.createClaudeCodeExecutor;
26962
28471
  } });
28472
+ var codex_1 = require_codex();
28473
+ Object.defineProperty(exports2, "createCodexExecutor", { enumerable: true, get: function() {
28474
+ return codex_1.createCodexExecutor;
28475
+ } });
28476
+ var copilot_1 = require_copilot();
28477
+ Object.defineProperty(exports2, "createCopilotExecutor", { enumerable: true, get: function() {
28478
+ return copilot_1.createCopilotExecutor;
28479
+ } });
26963
28480
  var http_api_1 = require_http_api();
26964
28481
  Object.defineProperty(exports2, "createHttpApiExecutor", { enumerable: true, get: function() {
26965
28482
  return http_api_1.createHttpApiExecutor;
@@ -27086,7 +28603,8 @@ var import_path = require("path");
27086
28603
  var import_promises = require("fs/promises");
27087
28604
  var import_yaml = __toESM(require_dist3());
27088
28605
  var import_shuttle = __toESM(require_dist5());
27089
- var buildVersion = true ? "0.30.23" : pkg.version;
28606
+ var buildVersion = true ? "0.32.0" : pkg.version;
28607
+ var AGENT_EXECUTORS = ["claude-code", "codex", "copilot"];
27090
28608
  var sifterBrand = {
27091
28609
  product: {
27092
28610
  id: "sifter",
@@ -27109,7 +28627,7 @@ var sifterBrand = {
27109
28627
  // shared config across multiple machines.
27110
28628
  configSearchPaths: ["~/.sifter/sifter.yaml"]
27111
28629
  },
27112
- executorAllowlist: ["claude-code", "anthropic-api", "http-api", "webhook", "custom-script"],
28630
+ executorAllowlist: ["claude-code", "codex", "copilot", "anthropic-api", "http-api", "webhook", "custom-script"],
27113
28631
  // What `init` configures when neither --executor nor an existing
27114
28632
  // sifter.yaml names one. The init pre-flight reads it too, to check git and
27115
28633
  // claude before the pairing code is spent.
@@ -27123,23 +28641,35 @@ var sifterBrand = {
27123
28641
  // newest of the flags every review passes (`--tools` arrived in 2.0.31;
27124
28642
  // the executor's REVIEW_RUN_ARGS names them all), and the hosted image pins
27125
28643
  // 2.1.x. Below it a review fails on an unknown option.
28644
+ //
28645
+ // `codex` and `copilot` are floored at the releases their containment was
28646
+ // verified against, and the floor is a containment rule rather than a
28647
+ // compatibility one: an older codex that does not know permission profiles
28648
+ // ignores the review's profile and falls back to a sandbox that reads the
28649
+ // whole disk, and an older copilot may not know a flag that keeps a tool or
28650
+ // a hook out of the run. Both executors are local only.
27126
28651
  minimumToolVersions: {
27127
28652
  git: "2.20.0",
27128
- claude: "2.0.45"
28653
+ claude: "2.0.45",
28654
+ codex: "0.162.0",
28655
+ copilot: "1.0.97"
27129
28656
  },
27130
28657
  // The overlay-fetch allowlist is NOT here: each tier signs its standards
27131
28658
  // URLs with its own Sift origin (SIFT_PUBLIC_API_URL), so the origin is a
27132
28659
  // property of the tier and lives on its `environments` entry below. A
27133
28660
  // Sifter paired to production allows production's origin and nothing else.
27134
- executorDefaults: {
27135
- "claude-code": {
27136
- // Transcript retention is a diagnostic switch a person flips for one
27137
- // session, so it rides the environment rather than a file they would
27138
- // have to remember to unset. The shared package reads no env of its
27139
- // own (BE-005); this brand is where the variable becomes config.
27140
- ...process.env.SHUTTLE_KEEP_TRANSCRIPTS ? { keepTranscripts: process.env.SHUTTLE_KEEP_TRANSCRIPTS } : {}
27141
- }
27142
- },
28661
+ executorDefaults: Object.fromEntries(
28662
+ AGENT_EXECUTORS.map((executor) => [
28663
+ executor,
28664
+ {
28665
+ // Transcript retention is a diagnostic switch a person flips for one
28666
+ // session, so it rides the environment rather than a file they would
28667
+ // have to remember to unset. The shared package reads no env of its
28668
+ // own (BE-005); this brand is where the variable becomes config.
28669
+ ...process.env.SHUTTLE_KEEP_TRANSCRIPTS ? { keepTranscripts: process.env.SHUTTLE_KEEP_TRANSCRIPTS } : {}
28670
+ }
28671
+ ])
28672
+ ),
27143
28673
  // The environments Whittle Labs operates, keyed by the profile name that
27144
28674
  // selects one. `init --profile stage` resolves both URLs from here, so a
27145
28675
  // non-prod pairing is one flag instead of three and there is no host to
@@ -27244,7 +28774,7 @@ var sifterBrand = {
27244
28774
  }
27245
28775
  const executorBlock = plan.keptBlock !== void 0 ? {
27246
28776
  ...plan.keptBlock,
27247
- ...executor === "claude-code" && ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
28777
+ ...AGENT_EXECUTORS.includes(executor) && ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
27248
28778
  } : await buildExecutorBlock(executor, ctx);
27249
28779
  const doc = {
27250
28780
  auth: {
@@ -27278,13 +28808,15 @@ var sifterBrand = {
27278
28808
  ]
27279
28809
  };
27280
28810
  function overlayOrigin(origin) {
27281
- return { "claude-code": { overlayAllowedHosts: [origin] } };
28811
+ return Object.fromEntries(AGENT_EXECUTORS.map((e) => [e, { overlayAllowedHosts: [origin] }]));
27282
28812
  }
27283
28813
  async function buildExecutorBlock(executor, ctx) {
27284
28814
  switch (executor) {
27285
- case "claude-code": {
28815
+ case "claude-code":
28816
+ case "codex":
28817
+ case "copilot": {
27286
28818
  return {
27287
- type: "claude-code",
28819
+ type: executor,
27288
28820
  ...ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
27289
28821
  };
27290
28822
  }