@whittlelabs/sifter 0.30.23 → 0.31.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 +2090 -786
  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,1226 @@ 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;
10020
10057
  }
10021
10058
  });
10022
10059
 
10023
- // ../../packages/shuttle/dist/executors/claude-code.js
10024
- var require_claude_code = __commonJS({
10025
- "../../packages/shuttle/dist/executors/claude-code.js"(exports2) {
10060
+ // ../../packages/shuttle/dist/executors/agent-process.js
10061
+ var require_agent_process = __commonJS({
10062
+ "../../packages/shuttle/dist/executors/agent-process.js"(exports2) {
10026
10063
  "use strict";
10027
10064
  var __importDefault = exports2 && exports2.__importDefault || function(mod) {
10028
10065
  return mod && mod.__esModule ? mod : { "default": mod };
10029
10066
  };
10030
10067
  Object.defineProperty(exports2, "__esModule", { value: true });
10031
- exports2.classifyEnvironmentFault = classifyEnvironmentFault;
10032
- exports2.createClaudeCodeExecutor = createClaudeCodeExecutor;
10033
- exports2.checkoutScopedTools = checkoutScopedTools;
10034
- exports2.parseStreamJson = parseStreamJson;
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/workspace-telemetry.js
10267
+ var require_workspace_telemetry = __commonJS({
10268
+ "../../packages/shuttle/dist/executors/workspace-telemetry.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.workspaceOutputs = workspaceOutputs;
10035
10275
  exports2.relativeWorkspaceReads = relativeWorkspaceReads;
10036
10276
  exports2.globToRegExp = globToRegExp;
10037
10277
  exports2.creditOverlayReads = creditOverlayReads;
10038
- exports2.extractJsonObject = extractJsonObject;
10039
- var child_process_1 = require("child_process");
10278
+ exports2.shellReadPaths = shellReadPaths;
10040
10279
  var fs_1 = require("fs");
10041
- var os_1 = __importDefault(require("os"));
10042
10280
  var path_1 = __importDefault(require("path"));
10043
- var string_decoder_1 = require("string_decoder");
10044
- var loom_1 = require_dist();
10045
- var types_1 = require_types2();
10046
- var validate_output_1 = require_validate_output();
10047
- var progress_1 = require_progress();
10048
- var apply_1 = require_apply();
10049
- var DEFAULT_TIMEOUT = 6e5;
10050
- var REVIEW_TOOLS = ["Read", "Grep", "Glob"];
10051
- function reviewRunArgs(settingSources) {
10281
+ function workspaceOutputs(workspace, runCwd, readPaths, toolInputs) {
10282
+ const reads = [...readPaths];
10283
+ for (const rel of creditOverlayReads(toolInputs, workspace.overlay?.writtenPaths ?? [], runCwd)) {
10284
+ reads.push(path_1.default.join(runCwd, rel));
10285
+ }
10052
10286
  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 })
10287
+ { label: "workspace_reads", content: { paths: relativeWorkspaceReads(reads, runCwd) } },
10288
+ {
10289
+ // Structured audit trail: reviewers and operators can see whether a
10290
+ // given review actually explored the checkout or fell back to the
10291
+ // diff (and why, including a failed private-repo auth), plus which
10292
+ // overlay files landed.
10293
+ label: "workspace",
10294
+ content: {
10295
+ materialized: workspace.materialized,
10296
+ ...workspace.warning ? { warning: workspace.warning } : {},
10297
+ ...workspace.context.length > 0 ? {
10298
+ context: workspace.context.map((c) => ({
10299
+ name: c.requestedName,
10300
+ materialized: c.materialized,
10301
+ ...c.warning ? { warning: c.warning } : {}
10302
+ }))
10303
+ } : {},
10304
+ ...workspace.overlay ? {
10305
+ overlay: {
10306
+ written: workspace.overlay.written,
10307
+ ...workspace.overlay.skipped.length > 0 ? { skipped: workspace.overlay.skipped } : {}
10308
+ }
10309
+ } : {}
10310
+ }
10311
+ }
10062
10312
  ];
10063
10313
  }
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}`;
10314
+ var MAX_REPORTED_READS = 2e3;
10315
+ function workspaceRoots(cwd) {
10316
+ const root = path_1.default.resolve(cwd);
10317
+ const roots = [root];
10318
+ try {
10319
+ const real = (0, fs_1.realpathSync)(root);
10320
+ if (real !== root)
10321
+ roots.push(real);
10322
+ } catch {
10323
+ }
10324
+ return roots;
10325
+ }
10326
+ function relativeWorkspaceReads(readPaths, cwd) {
10327
+ const roots = workspaceRoots(cwd);
10328
+ const root = roots[0];
10329
+ const out = /* @__PURE__ */ new Set();
10330
+ for (const p of readPaths) {
10331
+ const resolved = path_1.default.resolve(root, p);
10332
+ for (const r of roots) {
10333
+ if (resolved === r)
10334
+ break;
10335
+ if (!resolved.startsWith(r + path_1.default.sep))
10336
+ continue;
10337
+ out.add(resolved.slice(r.length + 1));
10338
+ break;
10339
+ }
10340
+ if (out.size >= MAX_REPORTED_READS)
10341
+ break;
10342
+ }
10343
+ return [...out].sort();
10344
+ }
10345
+ var GLOB_CHARS = /[*?[{]/;
10346
+ function escapeRegExp(s) {
10347
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10348
+ }
10349
+ function globToRegExp(glob) {
10350
+ let re = "^";
10351
+ for (let i = 0; i < glob.length; i++) {
10352
+ const ch = glob[i];
10353
+ if (ch === "*") {
10354
+ if (glob[i + 1] === "*") {
10355
+ if (glob[i + 2] === "/") {
10356
+ re += "(?:.*/)?";
10357
+ i += 2;
10358
+ } else {
10359
+ re += ".*";
10360
+ i += 1;
10361
+ }
10362
+ } else {
10363
+ re += "[^/]*";
10364
+ }
10365
+ } else if (ch === "?") {
10366
+ re += "[^/]";
10367
+ } else if (ch === "[") {
10368
+ const end = glob.indexOf("]", i + 1);
10369
+ if (end === -1)
10370
+ return null;
10371
+ let cls = glob.slice(i + 1, end);
10372
+ if (cls.startsWith("!"))
10373
+ cls = `^${cls.slice(1)}`;
10374
+ re += `[${cls.replace(/\\/g, "\\\\")}]`;
10375
+ i = end;
10376
+ } else if (ch === "{") {
10377
+ const end = glob.indexOf("}", i + 1);
10378
+ if (end === -1)
10379
+ return null;
10380
+ re += `(?:${glob.slice(i + 1, end).split(",").map(escapeRegExp).join("|")})`;
10381
+ i = end;
10382
+ } else {
10383
+ re += escapeRegExp(ch);
10384
+ }
10385
+ }
10386
+ return new RegExp(`${re}$`);
10387
+ }
10388
+ function creditOverlayReads(toolInputs, writtenPaths, runCwd) {
10389
+ if (writtenPaths.length === 0 || toolInputs.length === 0)
10390
+ return [];
10391
+ const roots = runCwd ? workspaceRoots(runCwd) : [];
10392
+ const dirs = /* @__PURE__ */ new Set();
10393
+ for (const rel of writtenPaths) {
10394
+ const parts = rel.split("/");
10395
+ for (let i = 1; i < parts.length; i++)
10396
+ dirs.add(`${parts.slice(0, i).join("/")}/`);
10397
+ }
10398
+ const dirAlt = [...dirs].map(escapeRegExp).join("|");
10399
+ const wordRe = dirs.size > 0 ? new RegExp(`(?:${dirAlt})[^\\s"'\`;|&<>()\\\\]*`, "g") : null;
10400
+ const findRe = dirs.size > 0 ? new RegExp(`\\bfind\\s+(?:\\./)?(${[...dirs].map((d) => escapeRegExp(d.slice(0, -1))).join("|")})/?(?=\\s|$)`, "g") : null;
10401
+ const readerRe = new RegExp(`(?:^|[\\s"])(?:-exec|-execdir|xargs)\\b[^;|&]*\\b${READERS}\\b`);
10402
+ const credited = /* @__PURE__ */ new Set();
10403
+ for (const raw of toolInputs) {
10404
+ const input = roots.reduce((s, r) => s.split(`${r}/`).join(""), raw);
10405
+ for (const rel of writtenPaths) {
10406
+ if (input.includes(rel))
10407
+ credited.add(rel);
10408
+ }
10409
+ if (wordRe) {
10410
+ for (const match of input.matchAll(wordRe)) {
10411
+ const word = match[0];
10412
+ if (!GLOB_CHARS.test(word))
10413
+ continue;
10414
+ const re = globToRegExp(word);
10415
+ if (!re)
10416
+ continue;
10417
+ for (const rel of writtenPaths) {
10418
+ if (re.test(rel))
10419
+ credited.add(rel);
10420
+ }
10421
+ }
10422
+ }
10423
+ if (findRe && readerRe.test(input)) {
10424
+ for (const match of input.matchAll(findRe)) {
10425
+ const dir = `${match[1]}/`;
10426
+ for (const rel of writtenPaths) {
10427
+ if (rel.startsWith(dir))
10428
+ credited.add(rel);
10429
+ }
10430
+ }
10431
+ }
10432
+ if (!PLAIN_READER_RE.test(input))
10433
+ continue;
10434
+ for (const match of input.matchAll(CD_RE)) {
10435
+ const target = match[1].replace(/^\.\//, "").replace(/\/+$/, "");
10436
+ const base = target.length > 0 ? `${target}/` : "";
10437
+ for (const rel of writtenPaths) {
10438
+ if (!rel.startsWith(base))
10439
+ continue;
10440
+ if (namesBareWord(input, rel.slice(base.length)))
10441
+ credited.add(rel);
10442
+ }
10443
+ }
10444
+ }
10445
+ return [...credited].sort();
10446
+ }
10447
+ var READERS = "(?:cat|head|tail|sed|awk|less|more|bat)";
10448
+ var PLAIN_READER_RE = new RegExp(`(?:^|[\\s"'\`;|&(])${READERS}\\b`);
10449
+ var CD_RE = /(?:^|[\s"'`;|&(])cd\s+(?:--\s+)?["']?([^\s"'`;|&<>()\\]+)/g;
10450
+ function namesBareWord(input, word) {
10451
+ const edge = String.raw`[\s"'\`;|&<>(),\\]`;
10452
+ return new RegExp(`(?:^|${edge})${escapeRegExp(word)}(?=$|${edge})`).test(input);
10453
+ }
10454
+ function shellReadPaths(commands, runCwd) {
10455
+ const roots = workspaceRoots(runCwd);
10456
+ const out = /* @__PURE__ */ new Set();
10457
+ for (const command of commands) {
10458
+ if (!PLAIN_READER_RE.test(command))
10459
+ continue;
10460
+ for (const raw of command.split(/[\s"'`;|&<>()]+/)) {
10461
+ const word = raw.replace(/^\.\//, "");
10462
+ if (word.length === 0 || word.startsWith("-") || GLOB_CHARS.test(word))
10463
+ continue;
10464
+ const abs = path_1.default.resolve(roots[0], roots.reduce((w, r) => w.startsWith(`${r}/`) ? w.slice(r.length + 1) : w, word));
10465
+ if (!roots.some((r) => abs.startsWith(r + path_1.default.sep)))
10466
+ continue;
10467
+ try {
10468
+ if ((0, fs_1.statSync)(abs).isFile())
10469
+ out.add(abs);
10470
+ } catch {
10471
+ }
10472
+ if (out.size >= MAX_REPORTED_READS)
10473
+ return [...out];
10474
+ }
10475
+ }
10476
+ return [...out];
10477
+ }
10478
+ }
10479
+ });
10480
+
10481
+ // ../../packages/shuttle/dist/executors/claude-code.js
10482
+ var require_claude_code = __commonJS({
10483
+ "../../packages/shuttle/dist/executors/claude-code.js"(exports2) {
10484
+ "use strict";
10485
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
10486
+ return mod && mod.__esModule ? mod : { "default": mod };
10487
+ };
10488
+ Object.defineProperty(exports2, "__esModule", { value: true });
10489
+ exports2.classifyEnvironmentFault = classifyEnvironmentFault;
10490
+ exports2.createClaudeCodeExecutor = createClaudeCodeExecutor;
10491
+ exports2.checkoutScopedTools = checkoutScopedTools;
10492
+ exports2.parseStreamJson = parseStreamJson;
10493
+ var fs_1 = require("fs");
10494
+ var os_1 = __importDefault(require("os"));
10495
+ var path_1 = __importDefault(require("path"));
10496
+ var loom_1 = require_dist();
10497
+ var types_1 = require_types2();
10498
+ var validate_output_1 = require_validate_output();
10499
+ var progress_1 = require_progress();
10500
+ var agent_process_1 = require_agent_process();
10501
+ var json_answer_1 = require_json_answer();
10502
+ var workspace_telemetry_1 = require_workspace_telemetry();
10503
+ var apply_1 = require_apply();
10504
+ var DEFAULT_TIMEOUT = 6e5;
10505
+ var REVIEW_TOOLS = ["Read", "Grep", "Glob"];
10506
+ function reviewRunArgs(settingSources) {
10507
+ return [
10508
+ "--setting-sources",
10509
+ settingSources,
10510
+ "--strict-mcp-config",
10511
+ "--tools",
10512
+ REVIEW_TOOLS.join(","),
10513
+ "--permission-mode",
10514
+ "default",
10515
+ "--settings",
10516
+ JSON.stringify({ disableAllHooks: true })
10517
+ ];
10518
+ }
10519
+ var RETIRED_READ_TOOLS = ["LS"];
10520
+ var HOSTED_ENV_PASSTHROUGH = [
10521
+ "PATH",
10522
+ "HOME",
10523
+ "TMPDIR",
10524
+ "LANG",
10525
+ "LC_ALL",
10526
+ "LC_CTYPE",
10527
+ "TZ",
10528
+ "NODE_EXTRA_CA_CERTS",
10529
+ "SSL_CERT_FILE",
10530
+ "SSL_CERT_DIR",
10531
+ "HTTPS_PROXY",
10532
+ "HTTP_PROXY",
10533
+ "NO_PROXY",
10534
+ "https_proxy",
10535
+ "http_proxy",
10536
+ "no_proxy"
10537
+ ];
10538
+ function describeClaudeFailure(meta, fallback) {
10539
+ if (!meta)
10540
+ return fallback;
10541
+ const status = meta.apiErrorStatus !== null ? ` (provider HTTP ${meta.apiErrorStatus})` : "";
10542
+ if (meta.detail)
10543
+ return `${meta.detail}${status}`;
10544
+ const subtype = meta.subtype && meta.subtype !== "success" ? `: ${meta.subtype}` : "";
10545
+ return `${fallback}${subtype}${status}`;
10546
+ }
10547
+ var READINESS_PROMPT = "Reply with the single word: ok";
10548
+ var READINESS_TIMEOUT_MS = 12e4;
10549
+ var INSTALL_REMEDY = "install Claude Code (npm install -g @anthropic-ai/claude-code) where this process can find it on PATH";
10550
+ 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";
10551
+ var LIMIT_REMEDY = "wait for the plan's usage limit to reset, or raise it";
10552
+ var SIGNED_OUT = /invalid api key|please run \/login|not (?:logged|signed) in|oauth token (?:has )?expired|authentication_error|invalid x-api-key/i;
10553
+ 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;
10554
+ var CLAUDE_FAULTS = {
10555
+ installRemedy: INSTALL_REMEDY,
10556
+ loginRemedy: LOGIN_REMEDY,
10557
+ limitRemedy: LIMIT_REMEDY,
10558
+ signedOut: SIGNED_OUT,
10559
+ outOfUsage: OUT_OF_USAGE
10560
+ };
10561
+ function classifyEnvironmentFault(failure) {
10562
+ return (0, agent_process_1.classifyAgentFault)(failure, CLAUDE_FAULTS);
10563
+ }
10564
+ function runFailure(local, message, facts) {
10565
+ return (0, agent_process_1.agentFailure)(local, message, facts, CLAUDE_FAULTS);
10566
+ }
10567
+ var ClaudeCodeExecutor = class {
10568
+ capability;
10569
+ strategies = [loom_1.PROMPT_EXECUTION_STRATEGY];
10570
+ config;
10571
+ constructor(config) {
10572
+ this.config = config;
10573
+ this.capability = { id: config.capabilityId };
10574
+ }
10575
+ estimate(dispatch) {
10576
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
10577
+ return {
10578
+ inputTokens: spec.costCapHint?.estimatedInputTokens,
10579
+ outputTokens: spec.costCapHint?.estimatedOutputTokens
10580
+ };
10581
+ }
10582
+ /**
10583
+ * Declare the run environment to the runner (ADR 0026): the opaque workspace
10584
+ * bag plus the materialization config (tmp dir, overlay allowlist). No
10585
+ * credentials here (ADR 0027) — the runner's broker reveals them on demand:
10586
+ * this executor pulls its model key from `ctx.credentials` in `execute()`,
10587
+ * and the git request references its own fetch credential. The executor
10588
+ * touches neither git nor Keep.
10589
+ */
10590
+ async prepareEnvironment(dispatch) {
10591
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
10592
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : void 0;
10593
+ return {
10594
+ ...spec.workspace ? { workspaceRequest: spec.workspace } : {},
10595
+ materializeOptions: {
10596
+ ...tmpBase ? { tmpDir: tmpBase } : {},
10597
+ overlayAllowedHosts: this.config.overlayAllowedHosts ?? []
10598
+ }
10599
+ };
10600
+ }
10601
+ async execute(dispatch, signal, ctx) {
10602
+ const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
10603
+ const workspace = ctx?.workspace ?? null;
10604
+ let modelKey = null;
10605
+ if (ctx?.credentials) {
10606
+ const hints = (0, loom_1.pickProviderHints)(spec, "anthropic-api");
10607
+ const customerId = typeof hints.customerId === "string" ? hints.customerId : void 0;
10608
+ if (!customerId) {
10609
+ throw new Error("claude-code dynamic-key mode requires providerHints.anthropicApi.customerId on every job");
10610
+ }
10611
+ modelKey = await ctx.credentials.reveal({ name: "managed-model-key", principal: customerId });
10612
+ }
10613
+ let jobScratch = null;
10614
+ let configDir;
10615
+ if (modelKey) {
10616
+ jobScratch = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "shuttle-cc-"));
10617
+ configDir = path_1.default.join(jobScratch, "claude-config");
10618
+ await fs_1.promises.mkdir(configDir);
10619
+ }
10620
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
10621
+ let promptOnlyScratch = null;
10622
+ const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
10623
+ try {
10624
+ const childEnv = buildChildEnv(modelKey, configDir);
10625
+ const claudeHints = (0, loom_1.pickProviderHints)(spec, "claude-code");
10626
+ const hintedModel = typeof claudeHints.model === "string" && claudeHints.model.length > 0 ? claudeHints.model : void 0;
10627
+ if (!workspace) {
10628
+ promptOnlyScratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-run-"));
10629
+ }
10630
+ const runCwd = workspace?.cwd ?? promptOnlyScratch;
10631
+ const { response, readPaths, toolInputs } = await this.runClaude(
10632
+ prompt,
10633
+ spec,
10634
+ runCwd,
10635
+ childEnv,
10636
+ signal,
10637
+ hintedModel,
10638
+ reporter,
10639
+ void 0,
10640
+ dispatch.job.id,
10641
+ // Local mode is the operator's own login: a failure of THAT is the
10642
+ // machine's, and the runner pauses on it. With a revealed key it is one
10643
+ // customer's, and only their job fails.
10644
+ modelKey === null
10645
+ );
10646
+ if (workspace) {
10647
+ response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, readPaths, toolInputs));
10648
+ }
10649
+ return response;
10650
+ } finally {
10651
+ reporter?.close();
10652
+ if (promptOnlyScratch)
10653
+ await fs_1.promises.rm(promptOnlyScratch, { recursive: true, force: true }).catch(() => {
10654
+ });
10655
+ if (jobScratch)
10656
+ await fs_1.promises.rm(jobScratch, { recursive: true, force: true }).catch(() => {
10657
+ });
10658
+ }
10659
+ }
10660
+ /**
10661
+ * In-session gate repair (ADR 0017): resume the session `execute()` returned a
10662
+ * handle for and correct its output. The repair turn works from the resumed
10663
+ * session's own context (no checkout needed), under the same `--json-schema`
10664
+ * constraint. A throwaway cwd hosts the run; the same handle rides back out so
10665
+ * a second repair round can resume again.
10666
+ */
10667
+ async continue(continuation, followUp, signal, _ctx) {
10668
+ const handle = continuation;
10669
+ if (!handle || typeof handle.sessionId !== "string") {
10670
+ throw new Error("claude-code continue: missing session to resume");
10671
+ }
10672
+ const tmpBase = this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
10673
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(tmpBase, "shuttle-cc-resume-"));
10674
+ try {
10675
+ const spec = {
10676
+ rendering: "producer",
10677
+ outputLabel: handle.outputLabel,
10678
+ outputSchema: handle.outputSchema
10679
+ };
10680
+ const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId, `${handle.sessionId}-repair`, handle.childEnv === void 0);
10681
+ return response;
10682
+ } finally {
10683
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
10684
+ });
10685
+ }
10686
+ }
10687
+ /**
10688
+ * Can this machine run a job now? One real one-turn call through the CLI,
10689
+ * the same probe `doctor --deep` makes. The runner asks it only while
10690
+ * claiming is paused on an `ExecutorEnvironmentError`, and only in local
10691
+ * mode. A signed-out or missing CLI fails before any model call, so the
10692
+ * re-check spends nothing until the login works again, and then one turn.
10693
+ */
10694
+ async checkReadiness(signal) {
10695
+ let run;
10696
+ try {
10697
+ run = await (0, agent_process_1.runAgentProcess)({
10698
+ bin: "claude",
10699
+ args: ["--print", "--output-format", "stream-json", "--verbose", "--max-turns", "1", READINESS_PROMPT],
10700
+ cwd: process.cwd(),
10701
+ idleTimeoutMs: READINESS_TIMEOUT_MS,
10702
+ toolName: "the claude CLI",
10703
+ signal
10704
+ });
10705
+ } catch (err) {
10706
+ const message = err instanceof Error ? err.message : String(err);
10707
+ return { ready: false, reason: message === "Dispatch aborted" ? "readiness check aborted" : message };
10708
+ }
10709
+ if (run.spawnError) {
10710
+ return {
10711
+ ready: false,
10712
+ reason: run.spawnError.code === "ENOENT" ? `the claude CLI is not on this machine's PATH (${run.spawnError.message})` : run.spawnError.message
10713
+ };
10714
+ }
10715
+ let meta = null;
10716
+ try {
10717
+ meta = parseStreamJson(run.stdout).resultMeta;
10718
+ } catch {
10719
+ meta = null;
10720
+ }
10721
+ if (run.code === 0 && !meta?.isError)
10722
+ return { ready: true };
10723
+ const said = meta?.detail ?? (0, agent_process_1.firstLine)(run.stderr) ?? (0, agent_process_1.firstLine)(run.stdout);
10724
+ return { ready: false, reason: said ?? `the claude CLI exited with code ${run.code}` };
10725
+ }
10726
+ async runClaude(prompt, spec, cwd, childEnv, signal, modelOverride, reporter, resumeSessionId, transcriptName, local = false) {
10727
+ const outputLabel = spec.outputLabel;
10728
+ const model = modelOverride ?? this.config.model;
10729
+ const allowedTools = local ? this.config.allowedTools : checkoutScopedTools(this.config.allowedTools);
10730
+ if (!local && this.config.claudeSettings === "user") {
10731
+ 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");
10732
+ }
10733
+ const settingSources = local && this.config.claudeSettings === "user" ? "user" : "";
10734
+ const args = ["--print", "--output-format", "stream-json", "--verbose", ...reviewRunArgs(settingSources)];
10735
+ if (resumeSessionId)
10736
+ args.push("--resume", resumeSessionId);
10737
+ if (spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0) {
10738
+ args.push("--json-schema", JSON.stringify(spec.outputSchema));
10739
+ }
10740
+ if (model)
10741
+ args.push("--model", model);
10742
+ if (this.config.maxTurns !== void 0)
10743
+ args.push("--max-turns", String(this.config.maxTurns));
10744
+ if (allowedTools && allowedTools.length > 0) {
10745
+ args.push("--allowedTools", allowedTools.join(","));
10746
+ }
10747
+ const tap = reporter ? new progress_1.StreamJsonTap({
10748
+ onReasoning: (t) => reporter.reasoning(t),
10749
+ onTool: (name, target) => reporter.tool(name, target),
10750
+ onResult: () => reporter.flush()
10751
+ }) : null;
10752
+ const run = await (0, agent_process_1.runAgentProcess)({
10753
+ bin: "claude",
10754
+ args,
10755
+ cwd,
10756
+ // Undefined in local mode → inherit the ambient env (and its
10757
+ // `~/.claude` login) unchanged.
10758
+ env: childEnv,
10759
+ stdin: prompt,
10760
+ idleTimeoutMs: this.config.timeout ?? DEFAULT_TIMEOUT,
10761
+ toolName: "Claude Code",
10762
+ signal,
10763
+ ...tap ? { onStdout: (text) => tap.push(text) } : {}
10764
+ });
10765
+ tap?.end();
10766
+ if (run.spawnError) {
10767
+ if (local && run.spawnError.code === "ENOENT") {
10768
+ throw new types_1.ExecutorEnvironmentError(`the claude CLI is not on this machine's PATH (${run.spawnError.message})`, INSTALL_REMEDY);
10769
+ }
10770
+ throw new Error(run.spawnError.message);
10771
+ }
10772
+ const { stdout, stderr: stderrText, code } = run;
10773
+ if (this.config.keepTranscripts && transcriptName) {
10774
+ void (0, agent_process_1.keepTranscript)((0, apply_1.expandHome)(this.config.keepTranscripts), transcriptName, stdout);
10775
+ }
10776
+ if (code !== 0) {
10777
+ let meta = null;
10778
+ try {
10779
+ meta = parseStreamJson(stdout).resultMeta;
10780
+ } catch {
10781
+ }
10782
+ const said = meta?.detail ?? (0, agent_process_1.firstLine)(stderrText) ?? (0, agent_process_1.firstLine)(stdout);
10783
+ const fallback = `Claude Code exited with code ${code}`;
10784
+ const failure = runFailure(local, describeClaudeFailure(meta, fallback), {
10785
+ apiErrorStatus: meta?.apiErrorStatus ?? null,
10786
+ text: said
10787
+ });
10788
+ if (failure instanceof types_1.ExecutorEnvironmentError && !meta && said) {
10789
+ throw new types_1.ExecutorEnvironmentError(`${fallback}: ${said}`, failure.remedy);
10790
+ }
10791
+ throw failure;
10792
+ }
10793
+ const stream = parseStreamJson(stdout);
10794
+ if (stream.resultMeta?.isError) {
10795
+ throw runFailure(local, describeClaudeFailure(stream.resultMeta, "Claude Code reported a failure"), {
10796
+ apiErrorStatus: stream.resultMeta.apiErrorStatus,
10797
+ text: stream.resultMeta.detail ?? (0, agent_process_1.firstLine)(stderrText)
10798
+ });
10799
+ }
10800
+ const answerText = stream.finalText ?? stdout;
10801
+ const parsed = stream.structuredOutput ?? (0, json_answer_1.extractJsonObject)(answerText);
10802
+ if (parsed === void 0 && stream.resultMeta) {
10803
+ 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))})`);
10804
+ }
10805
+ (0, validate_output_1.assertOutputValid)(spec, parsed);
10806
+ return {
10807
+ response: {
10808
+ outputs: [{ label: outputLabel, content: parsed ?? { raw: answerText } }],
10809
+ rawText: answerText,
10810
+ parsed,
10811
+ usage: {
10812
+ aiProvider: "claude-code",
10813
+ ...model ? { aiModel: model } : {},
10814
+ ...stream.usage ?? {}
10815
+ },
10816
+ // In-session repair handle (ADR 0017): present only when the run
10817
+ // reported a session id, so the runner can `--resume` it.
10818
+ ...stream.sessionId ? {
10819
+ continuation: {
10820
+ sessionId: stream.sessionId,
10821
+ model,
10822
+ childEnv,
10823
+ outputLabel,
10824
+ outputSchema: spec.outputSchema
10825
+ }
10826
+ } : {}
10827
+ },
10828
+ readPaths: stream.readPaths,
10829
+ toolInputs: stream.toolInputs
10830
+ };
10831
+ }
10832
+ };
10833
+ function createClaudeCodeExecutor(config, capabilityId = "claude-code") {
10834
+ const validated = { capabilityId };
10835
+ if (config.tmpDir !== void 0) {
10836
+ if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
10837
+ throw new Error('"tmpDir" must be a non-empty string');
10838
+ }
10839
+ validated.tmpDir = config.tmpDir;
10840
+ }
10841
+ if (config.model !== void 0) {
10842
+ if (typeof config.model !== "string")
10843
+ throw new Error('"model" must be a string');
10844
+ validated.model = config.model;
10845
+ }
10846
+ if (config.maxTurns !== void 0) {
10847
+ if (typeof config.maxTurns !== "number" || !Number.isInteger(config.maxTurns) || config.maxTurns < 1) {
10848
+ throw new Error('"maxTurns" must be a positive integer');
10849
+ }
10850
+ validated.maxTurns = config.maxTurns;
10851
+ }
10852
+ if (config.allowedTools !== void 0) {
10853
+ if (!Array.isArray(config.allowedTools) || !config.allowedTools.every((t) => typeof t === "string")) {
10854
+ throw new Error('"allowedTools" must be an array of strings');
10855
+ }
10856
+ validated.allowedTools = config.allowedTools;
10857
+ }
10858
+ if (config.timeout !== void 0) {
10859
+ if (typeof config.timeout !== "number" || config.timeout <= 0) {
10860
+ throw new Error('"timeout" must be a positive number');
10861
+ }
10862
+ validated.timeout = config.timeout;
10863
+ }
10864
+ if (config.keepTranscripts !== void 0) {
10865
+ if (typeof config.keepTranscripts !== "string" || config.keepTranscripts.length === 0) {
10866
+ throw new Error('"keepTranscripts" must be a non-empty directory path');
10867
+ }
10868
+ validated.keepTranscripts = config.keepTranscripts;
10869
+ }
10870
+ if (config.claudeSettings !== void 0) {
10871
+ if (config.claudeSettings !== "user") {
10872
+ throw new Error('"claudeSettings" may only be "user" (leave it unset to load no settings file)');
10873
+ }
10874
+ validated.claudeSettings = "user";
10875
+ }
10876
+ if (config.overlayAllowedHosts !== void 0) {
10877
+ if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
10878
+ throw new Error('"overlayAllowedHosts" must be an array of strings');
10879
+ }
10880
+ validated.overlayAllowedHosts = config.overlayAllowedHosts;
10881
+ }
10882
+ return new ClaudeCodeExecutor(validated);
10883
+ }
10884
+ function checkoutScopedTools(tools) {
10885
+ if (!tools)
10886
+ return void 0;
10887
+ const scoped = [];
10888
+ for (const tool of tools) {
10889
+ if (RETIRED_READ_TOOLS.some((t) => tool === t || tool === `${t}(./**)`))
10890
+ continue;
10891
+ const name = REVIEW_TOOLS.find((t) => tool === t || tool === `${t}(./**)`);
10892
+ if (!name) {
10893
+ throw new Error(`claude-code: a hosted run may use only ${REVIEW_TOOLS.join(", ")} inside the checkout; "${tool}" is not allowed`);
10894
+ }
10895
+ scoped.push(`${name}(./**)`);
10896
+ }
10897
+ return scoped;
10898
+ }
10899
+ function buildChildEnv(modelKey, configDir) {
10900
+ if (!modelKey && !configDir)
10901
+ return void 0;
10902
+ const env = {};
10903
+ for (const name of HOSTED_ENV_PASSTHROUGH) {
10904
+ if (process.env[name] !== void 0)
10905
+ env[name] = process.env[name];
10906
+ }
10907
+ return {
10908
+ ...env,
10909
+ ...modelKey ? { ANTHROPIC_API_KEY: modelKey } : {},
10910
+ ...configDir ? { CLAUDE_CONFIG_DIR: configDir } : {}
10911
+ };
10912
+ }
10913
+ function parseStreamJson(stdout) {
10914
+ let finalText = null;
10915
+ const readPaths = [];
10916
+ let usage = null;
10917
+ let resultMeta = null;
10918
+ let structuredOutput = null;
10919
+ let sessionId = null;
10920
+ const toolInputs = [];
10921
+ for (const line of stdout.split("\n")) {
10922
+ const trimmed = line.trim();
10923
+ if (!trimmed.startsWith("{"))
10924
+ continue;
10925
+ let event;
10926
+ try {
10927
+ event = JSON.parse(trimmed);
10928
+ } catch {
10929
+ continue;
10930
+ }
10931
+ if (typeof event.session_id === "string")
10932
+ sessionId = event.session_id;
10933
+ if (event.type === "assistant") {
10934
+ const content = event.message?.content;
10935
+ if (!Array.isArray(content))
10936
+ continue;
10937
+ for (const block of content) {
10938
+ const b = block;
10939
+ if (b?.type !== "tool_use")
10940
+ continue;
10941
+ const fp = b.input?.file_path;
10942
+ if (b.name === "Read" && typeof fp === "string") {
10943
+ readPaths.push(fp);
10944
+ }
10945
+ if (b.input && typeof b.input === "object")
10946
+ toolInputs.push(JSON.stringify(b.input));
10947
+ }
10948
+ } else if (event.type === "result") {
10949
+ if (typeof event.result === "string" && event.result.trim().length > 0) {
10950
+ finalText = event.result;
10951
+ }
10952
+ resultMeta = {
10953
+ subtype: typeof event.subtype === "string" ? event.subtype : null,
10954
+ isError: event.is_error === true,
10955
+ // On a failure the CLI puts a human-readable reason in `result` — the
10956
+ // model was unavailable, spend limit hit, and so on. It is the only
10957
+ // place that reason exists, so keep it for the failure message.
10958
+ detail: typeof event.result === "string" && event.result.trim().length > 0 ? event.result.trim() : null,
10959
+ apiErrorStatus: typeof event.api_error_status === "number" ? event.api_error_status : null
10960
+ };
10961
+ const so = event.structured_output;
10962
+ if (so && typeof so === "object" && !Array.isArray(so)) {
10963
+ structuredOutput = so;
10964
+ }
10965
+ const u = event.usage;
10966
+ const cost = event.total_cost_usd;
10967
+ usage = {
10968
+ ...typeof u?.input_tokens === "number" ? { inputTokens: u.input_tokens } : {},
10969
+ ...typeof u?.output_tokens === "number" ? { outputTokens: u.output_tokens } : {},
10970
+ ...typeof cost === "number" ? { costUsd: cost } : {}
10971
+ };
10972
+ }
10973
+ }
10974
+ return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, toolInputs };
10975
+ }
10976
+ }
10977
+ });
10978
+
10979
+ // ../../packages/shuttle/dist/executors/agent-common.js
10980
+ var require_agent_common = __commonJS({
10981
+ "../../packages/shuttle/dist/executors/agent-common.js"(exports2) {
10982
+ "use strict";
10983
+ Object.defineProperty(exports2, "__esModule", { value: true });
10984
+ exports2.DEFAULT_AGENT_TIMEOUT = void 0;
10985
+ exports2.parseAgentCommonConfig = parseAgentCommonConfig;
10986
+ exports2.agentEnvironment = agentEnvironment;
10987
+ exports2.refuseHosted = refuseHosted;
10988
+ var loom_1 = require_dist();
10989
+ var apply_1 = require_apply();
10990
+ exports2.DEFAULT_AGENT_TIMEOUT = 6e5;
10991
+ function parseAgentCommonConfig(config, capabilityId) {
10992
+ const out = { capabilityId };
10993
+ if (config.tmpDir !== void 0) {
10994
+ if (typeof config.tmpDir !== "string" || config.tmpDir.length === 0) {
10995
+ throw new Error('"tmpDir" must be a non-empty string');
10996
+ }
10997
+ out.tmpDir = config.tmpDir;
10998
+ }
10999
+ if (config.model !== void 0) {
11000
+ if (typeof config.model !== "string" || config.model.length === 0) {
11001
+ throw new Error('"model" must be a non-empty string');
11002
+ }
11003
+ out.model = config.model;
11004
+ }
11005
+ if (config.timeout !== void 0) {
11006
+ if (typeof config.timeout !== "number" || config.timeout <= 0) {
11007
+ throw new Error('"timeout" must be a positive number');
11008
+ }
11009
+ out.timeout = config.timeout;
11010
+ }
11011
+ if (config.keepTranscripts !== void 0) {
11012
+ if (typeof config.keepTranscripts !== "string" || config.keepTranscripts.length === 0) {
11013
+ throw new Error('"keepTranscripts" must be a non-empty directory path');
11014
+ }
11015
+ out.keepTranscripts = config.keepTranscripts;
11016
+ }
11017
+ if (config.overlayAllowedHosts !== void 0) {
11018
+ if (!Array.isArray(config.overlayAllowedHosts) || !config.overlayAllowedHosts.every((h) => typeof h === "string")) {
11019
+ throw new Error('"overlayAllowedHosts" must be an array of strings');
11020
+ }
11021
+ out.overlayAllowedHosts = config.overlayAllowedHosts;
11022
+ }
11023
+ return out;
11024
+ }
11025
+ function agentEnvironment(dispatch, config) {
11026
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
11027
+ const tmpBase = config.tmpDir ? (0, apply_1.expandHome)(config.tmpDir) : void 0;
11028
+ return {
11029
+ ...spec.workspace ? { workspaceRequest: spec.workspace } : {},
11030
+ materializeOptions: {
11031
+ ...tmpBase ? { tmpDir: tmpBase } : {},
11032
+ overlayAllowedHosts: config.overlayAllowedHosts ?? []
11033
+ }
11034
+ };
11035
+ }
11036
+ function refuseHosted(executor, hosted) {
11037
+ if (hosted) {
11038
+ 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`);
11039
+ }
11040
+ }
11041
+ }
11042
+ });
11043
+
11044
+ // ../../packages/shuttle/dist/executors/strict-schema.js
11045
+ var require_strict_schema = __commonJS({
11046
+ "../../packages/shuttle/dist/executors/strict-schema.js"(exports2) {
11047
+ "use strict";
11048
+ Object.defineProperty(exports2, "__esModule", { value: true });
11049
+ exports2.toStrict = toStrict;
11050
+ exports2.dropNulls = dropNulls;
11051
+ var UNSUPPORTED = ["format", "default", "$schema"];
11052
+ function toStrict(schema) {
11053
+ if (Array.isArray(schema))
11054
+ return schema.map(toStrict);
11055
+ if (!isSchema(schema))
11056
+ return schema;
11057
+ const out = {};
11058
+ for (const [key, value] of Object.entries(schema)) {
11059
+ if (UNSUPPORTED.includes(key))
11060
+ continue;
11061
+ if (key === "properties" && isSchema(value)) {
11062
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
11063
+ const props = {};
11064
+ for (const [name, prop] of Object.entries(value)) {
11065
+ const strict = toStrict(prop);
11066
+ props[name] = required.has(name) ? strict : nullable(strict);
11067
+ }
11068
+ out.properties = props;
11069
+ out.required = Object.keys(value);
11070
+ continue;
11071
+ }
11072
+ if (key === "required")
11073
+ continue;
11074
+ out[key] = SUBSCHEMA_KEYS.has(key) ? toStrict(value) : value;
11075
+ }
11076
+ if (out.type === "object" || out.properties !== void 0)
11077
+ out.additionalProperties = false;
11078
+ return out;
11079
+ }
11080
+ function dropNulls(value, schema) {
11081
+ if (!isSchema(schema))
11082
+ return value;
11083
+ if (Array.isArray(value)) {
11084
+ return isSchema(schema.items) ? value.map((v) => dropNulls(v, schema.items)) : value;
11085
+ }
11086
+ if (!isSchema(value)) {
11087
+ return value;
11088
+ }
11089
+ const branches = [schema.anyOf, schema.oneOf].find(Array.isArray);
11090
+ if (branches) {
11091
+ const branch = branches.find((b) => isSchema(b) && (b.type === "object" || isSchema(b.properties)));
11092
+ return branch ? dropNulls(value, branch) : value;
11093
+ }
11094
+ if (!isSchema(schema.properties))
11095
+ return value;
11096
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
11097
+ const out = {};
11098
+ for (const [name, v] of Object.entries(value)) {
11099
+ const prop = schema.properties[name];
11100
+ if (v === null && !required.has(name) && !allowsNull(prop))
11101
+ continue;
11102
+ out[name] = dropNulls(v, prop);
11103
+ }
11104
+ return out;
11105
+ }
11106
+ var SUBSCHEMA_KEYS = /* @__PURE__ */ new Set(["items", "anyOf", "oneOf", "allOf", "not", "additionalProperties", "definitions", "$defs"]);
11107
+ function nullable(schema) {
11108
+ if (!isSchema(schema) || allowsNull(schema))
11109
+ return schema;
11110
+ if (typeof schema.type === "string" && schema.anyOf === void 0 && schema.oneOf === void 0) {
11111
+ const out = { ...schema, type: [schema.type, "null"] };
11112
+ if (Array.isArray(schema.enum))
11113
+ out.enum = [...schema.enum, null];
11114
+ return out;
11115
+ }
11116
+ return { anyOf: [schema, { type: "null" }] };
11117
+ }
11118
+ function allowsNull(schema) {
11119
+ if (!isSchema(schema))
11120
+ return false;
11121
+ if (schema.type === "null")
11122
+ return true;
11123
+ if (Array.isArray(schema.type) && schema.type.includes("null"))
11124
+ return true;
11125
+ const branches = [schema.anyOf, schema.oneOf].find(Array.isArray);
11126
+ return branches?.some(allowsNull) ?? false;
11127
+ }
11128
+ function isSchema(v) {
11129
+ return typeof v === "object" && v !== null && !Array.isArray(v);
11130
+ }
11131
+ }
11132
+ });
11133
+
11134
+ // ../../packages/shuttle/dist/executors/codex.js
11135
+ var require_codex = __commonJS({
11136
+ "../../packages/shuttle/dist/executors/codex.js"(exports2) {
11137
+ "use strict";
11138
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
11139
+ return mod && mod.__esModule ? mod : { "default": mod };
11140
+ };
11141
+ Object.defineProperty(exports2, "__esModule", { value: true });
11142
+ exports2.resolveCodex = resolveCodex;
11143
+ exports2.codexReviewArgs = codexReviewArgs;
11144
+ exports2.parseCodexEvents = parseCodexEvents;
11145
+ exports2.createCodexExecutor = createCodexExecutor;
11146
+ var fs_1 = require("fs");
11147
+ var os_1 = __importDefault(require("os"));
11148
+ var path_1 = __importDefault(require("path"));
11149
+ var loom_1 = require_dist();
11150
+ var types_1 = require_types2();
11151
+ var validate_output_1 = require_validate_output();
11152
+ var progress_1 = require_progress();
11153
+ var agent_process_1 = require_agent_process();
11154
+ var agent_common_1 = require_agent_common();
11155
+ var json_answer_1 = require_json_answer();
11156
+ var strict_schema_1 = require_strict_schema();
11157
+ var workspace_telemetry_1 = require_workspace_telemetry();
11158
+ var apply_1 = require_apply();
11159
+ var CODEX_PROFILE = "sift-review";
11160
+ function reviewProfile(installDir, platform = process.platform) {
11161
+ const rules = ['":root"="deny"', '":minimal"="read"', '":workspace_roots"="read"', `${JSON.stringify(installDir)}="read"`];
11162
+ if (platform === "linux") {
11163
+ rules.push(`${JSON.stringify(path_1.default.join(codexHome(), "tmp", "arg0"))}="read"`);
11164
+ } else {
11165
+ rules.push('":tmpdir"="deny"', '":slash_tmp"="deny"');
11166
+ }
11167
+ return `permissions.${CODEX_PROFILE}={extends=":read-only", filesystem={${rules.join(", ")}}}`;
10091
11168
  }
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;
11169
+ function codexHome() {
11170
+ return process.env.CODEX_HOME ?? path_1.default.join(os_1.default.homedir(), ".codex");
11171
+ }
11172
+ function resolveCodex(pathEnv = process.env.PATH ?? "") {
11173
+ for (const dir of pathEnv.split(path_1.default.delimiter)) {
11174
+ if (!dir)
11175
+ continue;
11176
+ const candidate = path_1.default.join(dir, "codex");
11177
+ let real;
11178
+ try {
11179
+ real = (0, fs_1.realpathSync)(candidate);
11180
+ if (!(0, fs_1.statSync)(real).isFile())
11181
+ continue;
11182
+ } catch {
11183
+ continue;
11184
+ }
11185
+ const installDir = real.endsWith(".js") ? path_1.default.dirname(path_1.default.dirname(real)) : path_1.default.dirname(real);
11186
+ return { bin: real.endsWith(".js") ? candidate : real, installDir };
10108
11187
  }
10109
11188
  return null;
10110
11189
  }
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;
11190
+ var DISABLED_FEATURES = [
11191
+ "hooks",
11192
+ "apps",
11193
+ "plugins",
11194
+ "remote_plugin",
11195
+ "plugin_sharing",
11196
+ "skill_mcp_dependency_install",
11197
+ "browser_use",
11198
+ "browser_use_external",
11199
+ "browser_use_full_cdp_access",
11200
+ "browser_annotation_api",
11201
+ "computer_use",
11202
+ "image_generation",
11203
+ "workspace_dependencies"
11204
+ ];
11205
+ function requireCodex() {
11206
+ const codex = resolveCodex();
11207
+ if (!codex) {
11208
+ throw new types_1.ExecutorEnvironmentError("the codex CLI is not on this machine's PATH", CODEX_FAULTS.installRemedy);
10116
11209
  }
10117
- return null;
11210
+ return codex;
10118
11211
  }
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);
11212
+ function codexReviewArgs(installDir, platform = process.platform) {
11213
+ return [
11214
+ "--json",
11215
+ "--skip-git-repo-check",
11216
+ "--ignore-user-config",
11217
+ "--ignore-rules",
11218
+ "-c",
11219
+ reviewProfile(installDir, platform),
11220
+ "-c",
11221
+ `default_permissions="${CODEX_PROFILE}"`,
11222
+ "-c",
11223
+ 'approval_policy="never"',
11224
+ "-c",
11225
+ 'web_search="disabled"',
11226
+ ...DISABLED_FEATURES.flatMap((f) => ["--disable", f])
11227
+ ];
10124
11228
  }
10125
- var ClaudeCodeExecutor = class {
11229
+ var REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
11230
+ var CODEX_FAULTS = {
11231
+ installRemedy: "install Codex (brew install codex, or npm install -g @openai/codex) where this process can find it on PATH",
11232
+ loginRemedy: "run `codex login` and sign in (or `codex login --with-api-key`)",
11233
+ limitRemedy: "wait for the plan's usage limit to reset, or raise it",
11234
+ signedOut: /not logged in|run `?codex login|401 unauthorized|invalid api key|refresh token|token (?:has )?expired|sign in again/i,
11235
+ outOfUsage: /usage limit|hit your (?:\w+ )?limit|insufficient_quota|exceeded your current quota|429 too many requests/i
11236
+ };
11237
+ var READINESS_PROMPT = "Reply with the single word: ok";
11238
+ var READINESS_TIMEOUT_MS = 12e4;
11239
+ var CodexExecutor = class {
10126
11240
  capability;
10127
11241
  strategies = [loom_1.PROMPT_EXECUTION_STRATEGY];
10128
11242
  config;
@@ -10137,468 +11251,232 @@ var require_claude_code = __commonJS({
10137
11251
  outputTokens: spec.costCapHint?.estimatedOutputTokens
10138
11252
  };
10139
11253
  }
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
11254
  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
- };
11255
+ return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
10158
11256
  }
10159
11257
  async execute(dispatch, signal, ctx) {
11258
+ (0, agent_common_1.refuseHosted)("codex", Boolean(ctx?.credentials));
10160
11259
  const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
10161
11260
  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;
11261
+ const hints = (0, loom_1.pickProviderHints)(spec, "codex");
11262
+ const model = typeof hints.model === "string" && hints.model.length > 0 ? hints.model : this.config.model;
10180
11263
  const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
11264
+ const codex = requireCodex();
11265
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-codex-"));
10181
11266
  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(
11267
+ let runCwd = workspace?.cwd;
11268
+ if (!runCwd) {
11269
+ runCwd = path_1.default.join(scratch, "run");
11270
+ await fs_1.promises.mkdir(runCwd);
11271
+ }
11272
+ const { response, commands } = await this.runCodex({
11273
+ codex,
11274
+ args: ["exec", ...codexReviewArgs(codex.installDir), "-C", runCwd],
10190
11275
  prompt,
10191
11276
  spec,
10192
- runCwd,
10193
- childEnv,
11277
+ cwd: runCwd,
11278
+ scratch,
11279
+ model,
10194
11280
  signal,
10195
- hintedModel,
10196
11281
  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
- );
11282
+ transcriptName: dispatch.job.id
11283
+ });
10204
11284
  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
- });
11285
+ response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, (0, workspace_telemetry_1.shellReadPaths)(commands, runCwd), commands.map((command) => JSON.stringify({ command }))));
10233
11286
  }
10234
11287
  return response;
10235
11288
  } finally {
10236
11289
  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
- });
11290
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
11291
+ });
10243
11292
  }
10244
11293
  }
10245
11294
  /**
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.
11295
+ * In-session gate repair (ADR 0017): resume the thread and send the
11296
+ * follow-up under the same schema. The checkout is gone by now (the runner
11297
+ * cleans up before gates run), so the repair works from the thread's own
11298
+ * context in a scratch dir, as claude-code's does.
10251
11299
  */
10252
- async continue(continuation, followUp, signal, _ctx) {
11300
+ async continue(continuation, followUp, signal, ctx) {
11301
+ (0, agent_common_1.refuseHosted)("codex", Boolean(ctx?.credentials));
10253
11302
  const handle = continuation;
10254
- if (!handle || typeof handle.sessionId !== "string") {
10255
- throw new Error("claude-code continue: missing session to resume");
11303
+ if (!handle || typeof handle.threadId !== "string") {
11304
+ throw new Error("codex continue: missing thread to resume");
10256
11305
  }
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-"));
11306
+ const codex = requireCodex();
11307
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-codex-resume-"));
10259
11308
  try {
10260
11309
  const spec = {
10261
11310
  rendering: "producer",
10262
11311
  outputLabel: handle.outputLabel,
10263
11312
  outputSchema: handle.outputSchema
10264
11313
  };
10265
- const { response } = await this.runClaude(followUp, spec, scratch, handle.childEnv, signal, handle.model, null, handle.sessionId, `${handle.sessionId}-repair`, handle.childEnv === void 0);
11314
+ const { response } = await this.runCodex({
11315
+ codex,
11316
+ args: ["exec", "resume", handle.threadId, ...codexReviewArgs(codex.installDir)],
11317
+ prompt: followUp,
11318
+ spec,
11319
+ cwd: scratch,
11320
+ scratch,
11321
+ model: handle.model,
11322
+ signal,
11323
+ reporter: null,
11324
+ transcriptName: `${handle.threadId}-repair`
11325
+ });
10266
11326
  return response;
10267
11327
  } finally {
10268
11328
  await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
10269
11329
  });
10270
11330
  }
10271
11331
  }
10272
- /**
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.
10278
- */
10279
- async checkReadiness(signal) {
10280
- return new Promise((resolve) => {
10281
- let child;
10282
- try {
10283
- child = (0, child_process_1.spawn)("claude", ["--print", "--output-format", "stream-json", "--verbose", "--max-turns", "1", READINESS_PROMPT], { stdio: ["ignore", "pipe", "pipe"] });
10284
- } 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;
10310
- }
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({
10322
- 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;
10376
- }
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);
10405
- };
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
- }
10510
- });
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');
11332
+ /**
11333
+ * One real turn under the review flags, in a throwaway dir. A signed-out
11334
+ * CLI fails before any model call, so the re-check spends nothing until the
11335
+ * login works again; a spent plan is only visible to a real call.
11336
+ */
11337
+ async checkReadiness(signal) {
11338
+ const codex = resolveCodex();
11339
+ if (!codex)
11340
+ return { ready: false, reason: "the codex CLI is not on this machine's PATH" };
11341
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-codex-ready-"));
11342
+ try {
11343
+ let run;
11344
+ try {
11345
+ run = await (0, agent_process_1.runAgentProcess)({
11346
+ bin: codex.bin,
11347
+ args: ["exec", ...codexReviewArgs(codex.installDir), "--ephemeral", "-C", scratch, "-"],
11348
+ cwd: scratch,
11349
+ stdin: READINESS_PROMPT,
11350
+ idleTimeoutMs: READINESS_TIMEOUT_MS,
11351
+ toolName: "the codex CLI",
11352
+ signal
11353
+ });
11354
+ } catch (err) {
11355
+ const message = err instanceof Error ? err.message : String(err);
11356
+ return { ready: false, reason: message === "Dispatch aborted" ? "readiness check aborted" : message };
11357
+ }
11358
+ if (run.spawnError) {
11359
+ return {
11360
+ ready: false,
11361
+ reason: run.spawnError.code === "ENOENT" ? `the codex CLI is not on this machine's PATH (${run.spawnError.message})` : run.spawnError.message
11362
+ };
11363
+ }
11364
+ const events = parseCodexEvents(run.stdout);
11365
+ if (run.code === 0 && !events.failure)
11366
+ return { ready: true };
11367
+ return {
11368
+ ready: false,
11369
+ reason: events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? `the codex CLI exited with code ${run.code}`
11370
+ };
11371
+ } finally {
11372
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
11373
+ });
10536
11374
  }
10537
- validated.allowedTools = config.allowedTools;
10538
11375
  }
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');
11376
+ tmpBase() {
11377
+ return this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
11378
+ }
11379
+ async runCodex(opts) {
11380
+ const { spec, model } = opts;
11381
+ const args = [...opts.args];
11382
+ const hasSchema = spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0;
11383
+ if (hasSchema) {
11384
+ const schemaFile = path_1.default.join(opts.scratch, "output-schema.json");
11385
+ await fs_1.promises.writeFile(schemaFile, JSON.stringify((0, strict_schema_1.toStrict)(spec.outputSchema)));
11386
+ args.push("--output-schema", schemaFile);
11387
+ }
11388
+ if (model)
11389
+ args.push("-m", model);
11390
+ if (this.config.reasoningEffort)
11391
+ args.push("-c", `model_reasoning_effort="${this.config.reasoningEffort}"`);
11392
+ args.push("-");
11393
+ const tap = opts.reporter ? codexProgressTap(opts.reporter) : null;
11394
+ const run = await (0, agent_process_1.runAgentProcess)({
11395
+ bin: opts.codex.bin,
11396
+ args,
11397
+ cwd: opts.cwd,
11398
+ stdin: opts.prompt,
11399
+ idleTimeoutMs: this.config.timeout ?? agent_common_1.DEFAULT_AGENT_TIMEOUT,
11400
+ toolName: "Codex",
11401
+ signal: opts.signal,
11402
+ ...tap ? { onStdout: (text) => tap.push(text) } : {}
11403
+ });
11404
+ tap?.end();
11405
+ if (run.spawnError) {
11406
+ if (run.spawnError.code === "ENOENT") {
11407
+ throw new types_1.ExecutorEnvironmentError(`the codex CLI is not on this machine's PATH (${run.spawnError.message})`, CODEX_FAULTS.installRemedy);
11408
+ }
11409
+ throw new Error(run.spawnError.message);
10542
11410
  }
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');
11411
+ if (this.config.keepTranscripts) {
11412
+ void (0, agent_process_1.keepTranscript)((0, apply_1.expandHome)(this.config.keepTranscripts), opts.transcriptName, run.stdout);
10548
11413
  }
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)');
11414
+ const events = parseCodexEvents(run.stdout);
11415
+ if (run.code !== 0 || events.failure) {
11416
+ const said = events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? (0, agent_process_1.firstLine)(run.stdout);
11417
+ const message = said ? `Codex ${run.code === 0 ? "reported a failure" : `exited with code ${run.code}`}: ${said}` : `Codex exited with code ${run.code}`;
11418
+ throw (0, agent_process_1.agentFailure)(true, message, { apiErrorStatus: null, text: said }, CODEX_FAULTS);
10554
11419
  }
10555
- validated.claudeSettings = "user";
10556
- }
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');
11420
+ if (events.finalText === null) {
11421
+ throw new Error("Codex finished without a final message");
10560
11422
  }
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`);
11423
+ const answerText = events.finalText;
11424
+ const raw = (0, json_answer_1.extractJsonObject)(answerText);
11425
+ if (raw === void 0 && hasSchema) {
11426
+ throw new Error(`codex's final message contained no JSON object (message head: ${JSON.stringify(answerText.slice(0, 160))})`);
10575
11427
  }
10576
- scoped.push(`${name}(./**)`);
11428
+ const parsed = raw === void 0 ? void 0 : (0, strict_schema_1.dropNulls)(raw, spec.outputSchema);
11429
+ (0, validate_output_1.assertOutputValid)(spec, parsed);
11430
+ return {
11431
+ response: {
11432
+ outputs: [{ label: spec.outputLabel, content: parsed ?? { raw: answerText } }],
11433
+ rawText: answerText,
11434
+ parsed,
11435
+ usage: {
11436
+ aiProvider: "codex",
11437
+ ...model ? { aiModel: model } : {},
11438
+ ...events.usage ?? {}
11439
+ },
11440
+ ...events.threadId ? {
11441
+ continuation: {
11442
+ threadId: events.threadId,
11443
+ model,
11444
+ outputLabel: spec.outputLabel,
11445
+ outputSchema: spec.outputSchema
11446
+ }
11447
+ } : {}
11448
+ },
11449
+ commands: events.commands
11450
+ };
10577
11451
  }
10578
- return scoped;
11452
+ };
11453
+ function codexProgressTap(reporter) {
11454
+ return new progress_1.JsonLineTap((event) => {
11455
+ const item = event.item;
11456
+ const kind = item ? itemKind(item) : null;
11457
+ if (event.type === "item.started" && kind === "command_execution" && typeof item?.command === "string") {
11458
+ reporter.tool("shell", item.command);
11459
+ } else if (event.type === "item.completed" && kind === "reasoning" && typeof item?.text === "string") {
11460
+ reporter.reasoning(item.text);
11461
+ } else if (event.type === "turn.completed") {
11462
+ reporter.flush();
11463
+ }
11464
+ });
10579
11465
  }
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
- };
11466
+ function itemKind(item) {
11467
+ const kind = typeof item.type === "string" ? item.type : typeof item.item_type === "string" ? item.item_type : null;
11468
+ return kind === "assistant_message" ? "agent_message" : kind;
10593
11469
  }
10594
- function parseStreamJson(stdout) {
11470
+ function parseCodexEvents(stdout) {
11471
+ let threadId = null;
10595
11472
  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 = [];
11473
+ const commands = [];
11474
+ const seenCommands = /* @__PURE__ */ new Set();
11475
+ let input = 0;
11476
+ let output = 0;
11477
+ let sawUsage = false;
11478
+ let turnFailure = null;
11479
+ let lastError = null;
10602
11480
  for (const line of stdout.split("\n")) {
10603
11481
  const trimmed = line.trim();
10604
11482
  if (!trimmed.startsWith("{"))
@@ -10609,257 +11487,523 @@ var require_claude_code = __commonJS({
10609
11487
  } catch {
10610
11488
  continue;
10611
11489
  }
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);
11490
+ switch (event.type) {
11491
+ case "thread.started":
11492
+ if (typeof event.thread_id === "string")
11493
+ threadId = event.thread_id;
11494
+ break;
11495
+ case "item.started":
11496
+ case "item.updated":
11497
+ case "item.completed": {
11498
+ const item = event.item;
11499
+ if (!item)
11500
+ break;
11501
+ const kind = itemKind(item);
11502
+ if (kind === "agent_message" && event.type === "item.completed" && typeof item.text === "string") {
11503
+ finalText = item.text;
11504
+ lastError = null;
11505
+ } else if (kind === "command_execution" && typeof item.command === "string") {
11506
+ const key = typeof item.id === "string" ? item.id : item.command;
11507
+ if (!seenCommands.has(key)) {
11508
+ seenCommands.add(key);
11509
+ commands.push(item.command);
11510
+ }
10625
11511
  }
10626
- if (b.input && typeof b.input === "object")
10627
- toolInputs.push(JSON.stringify(b.input));
11512
+ break;
10628
11513
  }
10629
- } else if (event.type === "result") {
10630
- if (typeof event.result === "string" && event.result.trim().length > 0) {
10631
- finalText = event.result;
11514
+ case "turn.completed": {
11515
+ const u = event.usage;
11516
+ if (typeof u?.input_tokens === "number") {
11517
+ input += u.input_tokens;
11518
+ sawUsage = true;
11519
+ }
11520
+ if (typeof u?.output_tokens === "number") {
11521
+ output += u.output_tokens;
11522
+ sawUsage = true;
11523
+ }
11524
+ break;
10632
11525
  }
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;
11526
+ case "turn.failed": {
11527
+ const err = event.error;
11528
+ turnFailure = typeof err?.message === "string" ? err.message : "the turn failed";
11529
+ break;
10645
11530
  }
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
- };
11531
+ case "error":
11532
+ if (typeof event.message === "string")
11533
+ lastError = event.message;
11534
+ break;
11535
+ default:
11536
+ break;
10653
11537
  }
10654
11538
  }
10655
- return { finalText, readPaths, usage, resultMeta, structuredOutput, sessionId, toolInputs };
10656
- }
10657
- var MAX_REPORTED_READS = 2e3;
10658
- function workspaceRoots(cwd) {
10659
- const root = path_1.default.resolve(cwd);
10660
- const roots = [root];
10661
- try {
10662
- const real = (0, fs_1.realpathSync)(root);
10663
- if (real !== root)
10664
- roots.push(real);
10665
- } catch {
10666
- }
10667
- return roots;
11539
+ return {
11540
+ threadId,
11541
+ finalText,
11542
+ commands,
11543
+ usage: sawUsage ? { inputTokens: input, outputTokens: output } : null,
11544
+ failure: turnFailure ?? (finalText === null ? lastError : null)
11545
+ };
10668
11546
  }
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;
11547
+ function createCodexExecutor(config, capabilityId = "codex") {
11548
+ const validated = (0, agent_common_1.parseAgentCommonConfig)(config, capabilityId);
11549
+ if (config.reasoningEffort !== void 0) {
11550
+ if (!REASONING_EFFORTS.includes(config.reasoningEffort)) {
11551
+ throw new Error(`"reasoningEffort" must be one of ${REASONING_EFFORTS.join(", ")}`);
10682
11552
  }
10683
- if (out.size >= MAX_REPORTED_READS)
10684
- break;
11553
+ validated.reasoningEffort = config.reasoningEffort;
10685
11554
  }
10686
- return [...out].sort();
11555
+ return new CodexExecutor(validated);
10687
11556
  }
10688
- var GLOB_CHARS = /[*?[{]/;
10689
- function escapeRegExp(s) {
10690
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11557
+ }
11558
+ });
11559
+
11560
+ // ../../packages/shuttle/dist/executors/copilot.js
11561
+ var require_copilot = __commonJS({
11562
+ "../../packages/shuttle/dist/executors/copilot.js"(exports2) {
11563
+ "use strict";
11564
+ var __importDefault = exports2 && exports2.__importDefault || function(mod) {
11565
+ return mod && mod.__esModule ? mod : { "default": mod };
11566
+ };
11567
+ Object.defineProperty(exports2, "__esModule", { value: true });
11568
+ exports2.copilotReviewArgs = copilotReviewArgs;
11569
+ exports2.withSchema = withSchema;
11570
+ exports2.usageOf = usageOf;
11571
+ exports2.parseCopilotEvents = parseCopilotEvents;
11572
+ exports2.createCopilotExecutor = createCopilotExecutor;
11573
+ var crypto_1 = require("crypto");
11574
+ var fs_1 = require("fs");
11575
+ var os_1 = __importDefault(require("os"));
11576
+ var path_1 = __importDefault(require("path"));
11577
+ var loom_1 = require_dist();
11578
+ var types_1 = require_types2();
11579
+ var validate_output_1 = require_validate_output();
11580
+ var progress_1 = require_progress();
11581
+ var agent_process_1 = require_agent_process();
11582
+ var agent_common_1 = require_agent_common();
11583
+ var json_answer_1 = require_json_answer();
11584
+ var workspace_telemetry_1 = require_workspace_telemetry();
11585
+ var apply_1 = require_apply();
11586
+ var REVIEW_TOOLS = ["view", "grep", "glob"];
11587
+ var DENIED_TOOLS = ["shell", "write"];
11588
+ function copilotReviewArgs() {
11589
+ return [
11590
+ "--output-format",
11591
+ "json",
11592
+ "--no-color",
11593
+ "--allow-all-tools",
11594
+ `--available-tools=${REVIEW_TOOLS.join(",")}`,
11595
+ ...DENIED_TOOLS.map((t) => `--deny-tool=${t}`),
11596
+ "--disable-builtin-mcps",
11597
+ "--disallow-temp-dir",
11598
+ "--no-ask-user",
11599
+ "--no-auto-update",
11600
+ "--no-remote"
11601
+ ];
10691
11602
  }
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
- }
11603
+ var RUN_SETTINGS = { disableAllHooks: true, autoUpdate: false };
11604
+ var REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
11605
+ var COPILOT_FAULTS = {
11606
+ installRemedy: "install GitHub Copilot CLI (brew install --cask copilot-cli, or npm install -g @github/copilot) where this process can find it on PATH",
11607
+ 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)",
11608
+ 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",
11609
+ signedOut: /not (?:logged|signed) in|run `?copilot login|no authentication|authentication (?:failed|required)|bad credentials/i,
11610
+ outOfUsage: /exceeded your monthly quota|premium request|usage limit|hit your (?:\w+ )?limit|exceeded .*(?:allowance|limit)/i,
11611
+ notEntitled: {
11612
+ pattern: /access denied by policy|not authorized to use this copilot feature/i,
11613
+ 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
11614
  }
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("/")}/`);
11615
+ };
11616
+ var READINESS_PROMPT = "Reply with the single word: ok";
11617
+ var READINESS_TIMEOUT_MS = 12e4;
11618
+ var CopilotExecutor = class {
11619
+ capability;
11620
+ strategies = [loom_1.PROMPT_EXECUTION_STRATEGY];
11621
+ config;
11622
+ constructor(config) {
11623
+ this.config = config;
11624
+ this.capability = { id: config.capabilityId };
10740
11625
  }
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);
11626
+ estimate(dispatch) {
11627
+ const spec = (0, loom_1.readPromptExecutionSpec)(dispatch.inputs);
11628
+ return {
11629
+ inputTokens: spec.costCapHint?.estimatedInputTokens,
11630
+ outputTokens: spec.costCapHint?.estimatedOutputTokens
11631
+ };
11632
+ }
11633
+ async prepareEnvironment(dispatch) {
11634
+ return (0, agent_common_1.agentEnvironment)(dispatch, this.config);
11635
+ }
11636
+ async execute(dispatch, signal, ctx) {
11637
+ (0, agent_common_1.refuseHosted)("copilot", Boolean(ctx?.credentials));
11638
+ const { prompt, spec } = (0, loom_1.renderPromptExecution)(dispatch.inputs);
11639
+ const workspace = ctx?.workspace ?? null;
11640
+ const hints = (0, loom_1.pickProviderHints)(spec, "copilot");
11641
+ const model = typeof hints.model === "string" && hints.model.length > 0 ? hints.model : this.config.model;
11642
+ const reporter = spec.progressChannel && ctx?.reportProgress ? new progress_1.ProgressReporter((frames) => ctx.reportProgress(spec.progressChannel, frames)) : null;
11643
+ const runs = path_1.default.join(this.tmpBase(), "shuttle-copilot-runs");
11644
+ await fs_1.promises.mkdir(runs, { recursive: true, mode: 448 });
11645
+ await pruneRuns(runs);
11646
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(runs, "run-"));
11647
+ let keep = false;
11648
+ try {
11649
+ const home = await makeRunHome(scratch);
11650
+ let runCwd = workspace?.cwd;
11651
+ if (!runCwd) {
11652
+ runCwd = path_1.default.join(scratch, "run");
11653
+ await fs_1.promises.mkdir(runCwd);
11654
+ }
11655
+ const sessionId = (0, crypto_1.randomUUID)();
11656
+ const { response, reads, toolInputs } = await this.runCopilot({
11657
+ args: [...copilotReviewArgs(), `--session-id=${sessionId}`, "-C", runCwd],
11658
+ prompt: withSchema(prompt, spec),
11659
+ spec,
11660
+ cwd: runCwd,
11661
+ home,
11662
+ scratch,
11663
+ model,
11664
+ signal,
11665
+ reporter,
11666
+ transcriptName: dispatch.job.id
11667
+ });
11668
+ response.continuation = {
11669
+ sessionId,
11670
+ home,
11671
+ model,
11672
+ outputLabel: spec.outputLabel,
11673
+ outputSchema: spec.outputSchema
11674
+ };
11675
+ keep = true;
11676
+ if (workspace)
11677
+ response.outputs.push(...(0, workspace_telemetry_1.workspaceOutputs)(workspace, runCwd, reads, toolInputs));
11678
+ return response;
11679
+ } finally {
11680
+ reporter?.close();
11681
+ if (!keep)
11682
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
11683
+ });
10752
11684
  }
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
- }
11685
+ }
11686
+ /** In-session gate repair (ADR 0017): resume the session this executor named, from the run's own COPILOT_HOME. */
11687
+ async continue(continuation, followUp, signal, ctx) {
11688
+ (0, agent_common_1.refuseHosted)("copilot", Boolean(ctx?.credentials));
11689
+ const handle = continuation;
11690
+ if (!handle || typeof handle.sessionId !== "string" || typeof handle.home !== "string") {
11691
+ throw new Error("copilot continue: missing session to resume");
10766
11692
  }
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
- }
11693
+ const scratch = path_1.default.dirname(handle.home);
11694
+ const cwd = path_1.default.join(scratch, "resume");
11695
+ await fs_1.promises.mkdir(cwd, { recursive: true });
11696
+ const spec = {
11697
+ rendering: "producer",
11698
+ outputLabel: handle.outputLabel,
11699
+ outputSchema: handle.outputSchema
11700
+ };
11701
+ const { response } = await this.runCopilot({
11702
+ args: [...copilotReviewArgs(), `--resume=${handle.sessionId}`, "-C", cwd],
11703
+ prompt: followUp,
11704
+ spec,
11705
+ cwd,
11706
+ home: handle.home,
11707
+ scratch,
11708
+ model: handle.model,
11709
+ signal,
11710
+ reporter: null,
11711
+ transcriptName: `${handle.sessionId}-repair`
11712
+ });
11713
+ response.continuation = handle;
11714
+ return response;
11715
+ }
11716
+ /** One real turn under the review flags. A signed-out CLI fails before any model call. */
11717
+ async checkReadiness(signal) {
11718
+ const scratch = await fs_1.promises.mkdtemp(path_1.default.join(this.tmpBase(), "shuttle-copilot-ready-"));
11719
+ try {
11720
+ const home = await makeRunHome(scratch);
11721
+ let run;
11722
+ try {
11723
+ run = await withOperatorLogin(home, () => (0, agent_process_1.runAgentProcess)({
11724
+ bin: "copilot",
11725
+ args: [...copilotReviewArgs(), "-C", scratch, "-p", READINESS_PROMPT],
11726
+ cwd: scratch,
11727
+ env: runEnv(home),
11728
+ idleTimeoutMs: READINESS_TIMEOUT_MS,
11729
+ toolName: "the copilot CLI",
11730
+ signal
11731
+ }));
11732
+ } catch (err) {
11733
+ const message = err instanceof Error ? err.message : String(err);
11734
+ return { ready: false, reason: message === "Dispatch aborted" ? "readiness check aborted" : message };
10774
11735
  }
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);
11736
+ if (run.spawnError) {
11737
+ return {
11738
+ ready: false,
11739
+ reason: run.spawnError.code === "ENOENT" ? `the copilot CLI is not on this machine's PATH (${run.spawnError.message})` : run.spawnError.message
11740
+ };
10786
11741
  }
11742
+ const events = parseCopilotEvents(run.stdout);
11743
+ if (run.code === 0 && !events.failure)
11744
+ return { ready: true };
11745
+ return {
11746
+ ready: false,
11747
+ reason: events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? `the copilot CLI exited with code ${run.code}`
11748
+ };
11749
+ } finally {
11750
+ await fs_1.promises.rm(scratch, { recursive: true, force: true }).catch(() => {
11751
+ });
10787
11752
  }
10788
11753
  }
10789
- return [...credited].sort();
11754
+ tmpBase() {
11755
+ return this.config.tmpDir ? (0, apply_1.expandHome)(this.config.tmpDir) : os_1.default.tmpdir();
11756
+ }
11757
+ async runCopilot(opts) {
11758
+ const { spec, model } = opts;
11759
+ const usageFile = path_1.default.join(opts.scratch, `usage-${(0, crypto_1.randomUUID)()}.json`);
11760
+ const args = [...opts.args, `--usage-output-file=${usageFile}`, `--log-dir=${path_1.default.join(opts.home, "logs")}`];
11761
+ if (model)
11762
+ args.push(`--model=${model}`);
11763
+ if (this.config.reasoningEffort)
11764
+ args.push(`--reasoning-effort=${this.config.reasoningEffort}`);
11765
+ const tap = opts.reporter ? copilotProgressTap(opts.reporter) : null;
11766
+ const run = await withOperatorLogin(opts.home, () => (0, agent_process_1.runAgentProcess)({
11767
+ bin: "copilot",
11768
+ args,
11769
+ cwd: opts.cwd,
11770
+ env: runEnv(opts.home),
11771
+ // The prompt comes from stdin: a review prompt carries the diff, far
11772
+ // past what argv should hold, and argv is visible to every process.
11773
+ stdin: opts.prompt,
11774
+ idleTimeoutMs: this.config.timeout ?? agent_common_1.DEFAULT_AGENT_TIMEOUT,
11775
+ toolName: "Copilot",
11776
+ signal: opts.signal,
11777
+ ...tap ? { onStdout: (text) => tap.push(text) } : {}
11778
+ }));
11779
+ tap?.end();
11780
+ if (run.spawnError) {
11781
+ if (run.spawnError.code === "ENOENT") {
11782
+ throw new types_1.ExecutorEnvironmentError(`the copilot CLI is not on this machine's PATH (${run.spawnError.message})`, COPILOT_FAULTS.installRemedy);
11783
+ }
11784
+ throw new Error(run.spawnError.message);
11785
+ }
11786
+ if (this.config.keepTranscripts) {
11787
+ void (0, agent_process_1.keepTranscript)((0, apply_1.expandHome)(this.config.keepTranscripts), opts.transcriptName, run.stdout);
11788
+ }
11789
+ const events = parseCopilotEvents(run.stdout);
11790
+ if (run.code !== 0 || events.failure) {
11791
+ const said = events.failure ?? (0, agent_process_1.firstLine)(run.stderr) ?? (0, agent_process_1.firstLine)(run.stdout);
11792
+ const message = said ? `Copilot ${run.code === 0 ? "reported a failure" : `exited with code ${run.code}`}: ${said}` : `Copilot exited with code ${run.code}`;
11793
+ throw (0, agent_process_1.agentFailure)(true, message, { apiErrorStatus: null, text: said }, COPILOT_FAULTS);
11794
+ }
11795
+ if (events.finalText === null)
11796
+ throw new Error("Copilot finished without a final message");
11797
+ const answerText = events.finalText;
11798
+ const parsed = (0, json_answer_1.extractJsonObject)(answerText);
11799
+ if (parsed === void 0 && hasSchema(spec)) {
11800
+ throw new Error(`copilot's final message contained no JSON object (message head: ${JSON.stringify(answerText.slice(0, 160))})`);
11801
+ }
11802
+ (0, validate_output_1.assertOutputValid)(spec, parsed);
11803
+ const usage = await readUsage(usageFile);
11804
+ return {
11805
+ response: {
11806
+ outputs: [{ label: spec.outputLabel, content: parsed ?? { raw: answerText } }],
11807
+ rawText: answerText,
11808
+ parsed,
11809
+ // The model the session actually ran, from its usage file, over the one asked for.
11810
+ usage: { aiProvider: "copilot", ...model ? { aiModel: model } : {}, ...usage ?? {} }
11811
+ },
11812
+ reads: events.readPaths,
11813
+ toolInputs: events.toolInputs
11814
+ };
11815
+ }
11816
+ };
11817
+ function hasSchema(spec) {
11818
+ return Boolean(spec.outputSchema && typeof spec.outputSchema === "object" && Object.keys(spec.outputSchema).length > 0);
10790
11819
  }
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);
11820
+ function withSchema(prompt, spec) {
11821
+ if (!hasSchema(spec))
11822
+ return prompt;
11823
+ return `${prompt}
11824
+
11825
+ ---
11826
+
11827
+ Your final message must be one JSON object, and nothing else, that validates against this JSON Schema:
11828
+
11829
+ ${JSON.stringify(spec.outputSchema)}
11830
+ `;
10796
11831
  }
10797
- async function keepTranscript(dir, name, stdout) {
11832
+ var RUN_RETENTION_MS = 24 * 60 * 60 * 1e3;
11833
+ async function pruneRuns(runs) {
11834
+ let entries;
10798
11835
  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);
11836
+ entries = await fs_1.promises.readdir(runs);
11837
+ } catch {
11838
+ return;
11839
+ }
11840
+ const cutoff = Date.now() - RUN_RETENTION_MS;
11841
+ await Promise.all(entries.map(async (name) => {
11842
+ const dir = path_1.default.join(runs, name);
11843
+ try {
11844
+ if ((await fs_1.promises.stat(dir)).mtimeMs < cutoff)
11845
+ await fs_1.promises.rm(dir, { recursive: true, force: true });
11846
+ } catch {
11847
+ }
11848
+ }));
11849
+ }
11850
+ async function makeRunHome(scratch) {
11851
+ const home = path_1.default.join(scratch, "copilot-home");
11852
+ await fs_1.promises.mkdir(home, { mode: 448 });
11853
+ await fs_1.promises.writeFile(path_1.default.join(home, "settings.json"), JSON.stringify(RUN_SETTINGS));
11854
+ const bin = path_1.default.join(scratch, "bin");
11855
+ await fs_1.promises.mkdir(bin, { recursive: true });
11856
+ await fs_1.promises.writeFile(path_1.default.join(bin, "gh"), REFUSING_GH, { mode: 493 });
11857
+ return home;
11858
+ }
11859
+ var REFUSING_GH = '#!/bin/sh\necho "gh is not available to a Sifter review" >&2\nexit 1\n';
11860
+ var LOGIN_POINTER_KEYS = ["lastLoggedInUser", "loggedInUsers"];
11861
+ var LOGIN_TOKEN_KEYS = ["authTokens"];
11862
+ async function withOperatorLogin(home, fn) {
11863
+ const file = path_1.default.join(home, "config.json");
11864
+ const login = await operatorLogin();
11865
+ if (login)
11866
+ await fs_1.promises.writeFile(file, JSON.stringify(login), { mode: 384 });
11867
+ else
11868
+ await fs_1.promises.rm(file, { force: true });
11869
+ try {
11870
+ return await fn();
11871
+ } finally {
11872
+ await forgetToken(file);
11873
+ }
11874
+ }
11875
+ async function forgetToken(file) {
11876
+ let config;
11877
+ try {
11878
+ config = await readJsonc(file);
10802
11879
  } catch (err) {
10803
- console.warn(`[shuttle] could not keep the transcript for ${name} under ${dir}: ${err instanceof Error ? err.message : String(err)}`);
11880
+ if (err.code !== "ENOENT")
11881
+ await fs_1.promises.rm(file, { force: true }).catch(() => {
11882
+ });
11883
+ return;
10804
11884
  }
11885
+ if (!LOGIN_TOKEN_KEYS.some((k) => k in config))
11886
+ return;
11887
+ for (const k of LOGIN_TOKEN_KEYS)
11888
+ delete config[k];
11889
+ await fs_1.promises.writeFile(file, JSON.stringify(config), { mode: 384 }).catch(() => fs_1.promises.rm(file, { force: true }));
10805
11890
  }
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++;
11891
+ async function operatorLogin() {
11892
+ const home = process.env.COPILOT_HOME ?? path_1.default.join(os_1.default.homedir(), ".copilot");
11893
+ try {
11894
+ const parsed = await readJsonc(path_1.default.join(home, "config.json"));
11895
+ const keys = [...LOGIN_POINTER_KEYS, ...LOGIN_TOKEN_KEYS].filter((k) => k in parsed);
11896
+ return keys.length > 0 ? Object.fromEntries(keys.map((k) => [k, parsed[k]])) : null;
11897
+ } catch {
11898
+ return null;
11899
+ }
11900
+ }
11901
+ async function readJsonc(file) {
11902
+ const raw = await fs_1.promises.readFile(file, "utf8");
11903
+ return JSON.parse(raw.split("\n").filter((line) => !line.trim().startsWith("//")).join("\n"));
11904
+ }
11905
+ function runEnv(home) {
11906
+ const bin = path_1.default.join(path_1.default.dirname(home), "bin");
11907
+ return {
11908
+ ...process.env,
11909
+ PATH: `${bin}${path_1.default.delimiter}${process.env.PATH ?? ""}`,
11910
+ COPILOT_HOME: home,
11911
+ COPILOT_AUTO_UPDATE: "false"
11912
+ };
11913
+ }
11914
+ async function readUsage(file) {
11915
+ try {
11916
+ const raw = JSON.parse(await fs_1.promises.readFile(file, "utf8"));
11917
+ return usageOf(raw);
11918
+ } catch {
11919
+ return null;
11920
+ }
11921
+ }
11922
+ function usageOf(raw) {
11923
+ const models = Object.entries(raw.modelMetrics ?? {});
11924
+ if (models.length === 0)
11925
+ return null;
11926
+ const num = (v) => typeof v === "number" ? v : 0;
11927
+ let input = 0;
11928
+ let output = 0;
11929
+ for (const [, m] of models) {
11930
+ input += num(m.usage?.inputTokens) + num(m.usage?.cacheReadTokens);
11931
+ output += num(m.usage?.outputTokens);
11932
+ }
11933
+ return { inputTokens: input, outputTokens: output, ...models.length === 1 ? { aiModel: models[0][0] } : {} };
11934
+ }
11935
+ function copilotProgressTap(reporter) {
11936
+ return new progress_1.JsonLineTap((event) => {
11937
+ const data = event.data ?? {};
11938
+ if (event.type === "tool.execution_start" && typeof data.toolName === "string") {
11939
+ const args = data.arguments ?? {};
11940
+ const target = [args.path, args.pattern].find((v) => typeof v === "string");
11941
+ reporter.tool(data.toolName, target);
11942
+ } else if (event.type === "assistant.reasoning" && typeof data.content === "string") {
11943
+ reporter.reasoning(data.content);
11944
+ } else if (event.type === "assistant.turn_end") {
11945
+ reporter.flush();
11946
+ }
11947
+ });
11948
+ }
11949
+ function parseCopilotEvents(stdout) {
11950
+ let finalText = null;
11951
+ const readPaths = [];
11952
+ const toolInputs = [];
11953
+ const started = /* @__PURE__ */ new Map();
11954
+ let lastError = null;
11955
+ for (const line of stdout.split("\n")) {
11956
+ const trimmed = line.trim();
11957
+ if (!trimmed.startsWith("{"))
11958
+ continue;
11959
+ let event;
11960
+ try {
11961
+ event = JSON.parse(trimmed);
11962
+ } catch {
10815
11963
  continue;
10816
11964
  }
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;
11965
+ const data = event.data ?? {};
11966
+ switch (event.type) {
11967
+ case "tool.execution_start": {
11968
+ if (typeof data.toolCallId !== "string" || typeof data.toolName !== "string")
10843
11969
  break;
11970
+ const args = data.arguments ?? {};
11971
+ started.set(data.toolCallId, { name: data.toolName, args });
11972
+ toolInputs.push(JSON.stringify(args));
11973
+ break;
11974
+ }
11975
+ case "tool.execution_complete": {
11976
+ const call = typeof data.toolCallId === "string" ? started.get(data.toolCallId) : void 0;
11977
+ if (call && data.success === true && call.name === "view" && typeof call.args.path === "string") {
11978
+ readPaths.push(call.args.path);
10844
11979
  }
11980
+ break;
10845
11981
  }
11982
+ case "assistant.message":
11983
+ if (typeof data.content === "string" && data.content.trim().length > 0) {
11984
+ finalText = data.content;
11985
+ lastError = null;
11986
+ }
11987
+ break;
11988
+ default:
11989
+ if (typeof event.type === "string" && /(^|\.)error$/.test(event.type)) {
11990
+ const message = [data.message, data.error?.message].find((v) => typeof v === "string");
11991
+ if (message)
11992
+ lastError = message;
11993
+ }
10846
11994
  }
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
11995
  }
10854
- return last;
11996
+ return { finalText, readPaths, toolInputs, failure: finalText === null ? lastError : null };
10855
11997
  }
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;
11998
+ function createCopilotExecutor(config, capabilityId = "copilot") {
11999
+ const validated = (0, agent_common_1.parseAgentCommonConfig)(config, capabilityId);
12000
+ if (config.reasoningEffort !== void 0) {
12001
+ if (!REASONING_EFFORTS.includes(config.reasoningEffort)) {
12002
+ throw new Error(`"reasoningEffort" must be one of ${REASONING_EFFORTS.join(", ")}`);
12003
+ }
12004
+ validated.reasoningEffort = config.reasoningEffort;
10862
12005
  }
12006
+ return new CopilotExecutor(validated);
10863
12007
  }
10864
12008
  }
10865
12009
  });
@@ -12502,6 +13646,8 @@ var require_checks = __commonJS({
12502
13646
  var path_1 = require("path");
12503
13647
  var registry_1 = require_registry();
12504
13648
  var claude_code_1 = require_claude_code();
13649
+ var codex_1 = require_codex();
13650
+ var copilot_1 = require_copilot();
12505
13651
  var http_api_1 = require_http_api();
12506
13652
  var anthropic_api_1 = require_anthropic_api();
12507
13653
  var webhook_1 = require_webhook();
@@ -12513,9 +13659,21 @@ var require_checks = __commonJS({
12513
13659
  var scaffold_1 = require_scaffold();
12514
13660
  var NODE_FLOOR = "20.0.0";
12515
13661
  var DISK_FLOOR_BYTES = 1024 * 1024 * 1024;
12516
- var WORKSPACE_EXECUTOR = "claude-code";
13662
+ var AGENTS = [
13663
+ { executor: "claude-code", bin: "claude", install: "install Claude Code (npm install -g @anthropic-ai/claude-code)" },
13664
+ { executor: "codex", bin: "codex", install: "install Codex (brew install codex, or npm install -g @openai/codex)" },
13665
+ {
13666
+ executor: "copilot",
13667
+ bin: "copilot",
13668
+ install: "install GitHub Copilot CLI (brew install --cask copilot-cli, or npm install -g @github/copilot)"
13669
+ }
13670
+ ];
13671
+ var AGENT_EXECUTORS2 = AGENTS.map((a) => a.executor);
13672
+ var DEFAULT_AGENT = "claude-code";
12517
13673
  var BUILT_IN_EXECUTORS = [
12518
13674
  ["claude-code", claude_code_1.createClaudeCodeExecutor],
13675
+ ["codex", codex_1.createCodexExecutor],
13676
+ ["copilot", copilot_1.createCopilotExecutor],
12519
13677
  ["http-api", http_api_1.createHttpApiExecutor],
12520
13678
  ["anthropic-api", anthropic_api_1.createAnthropicApiExecutor],
12521
13679
  ["webhook", webhook_1.createWebhookExecutor],
@@ -12544,22 +13702,27 @@ var require_checks = __commonJS({
12544
13702
  function configuredExecutors(config) {
12545
13703
  return Object.keys(config.executors ?? {});
12546
13704
  }
12547
- function claudeCodeSettings(config, subject) {
12548
- const user = config.executors[WORKSPACE_EXECUTOR];
12549
- if (user === void 0)
13705
+ function activeAgent(config) {
13706
+ const configured = configuredExecutors(config);
13707
+ const pooled = config.pools.map((p) => p.executor).find((e) => AGENT_EXECUTORS2.includes(e) && configured.includes(e));
13708
+ return pooled ?? configured.find((e) => AGENT_EXECUTORS2.includes(e)) ?? null;
13709
+ }
13710
+ function agentSettings(config, subject) {
13711
+ const executor = activeAgent(config);
13712
+ if (executor === null)
12550
13713
  return null;
12551
- const defaults = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[WORKSPACE_EXECUTOR] ?? {};
12552
- return { ...defaults, ...user };
13714
+ const user = config.executors[executor] ?? {};
13715
+ const defaults = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[executor] ?? {};
13716
+ return { executor, settings: { ...defaults, ...user } };
12553
13717
  }
12554
13718
  function claudeSettingsOf(config) {
12555
- return config.executors[WORKSPACE_EXECUTOR]?.claudeSettings;
13719
+ return config.executors["claude-code"]?.claudeSettings;
12556
13720
  }
12557
13721
  function describeLogin(status) {
12558
13722
  return [status.authMethod ?? "unknown method", status.subscriptionType].filter((part) => part !== null).join(", ");
12559
13723
  }
12560
13724
  function runRoot(config, subject, osTmpDir) {
12561
- const settings = claudeCodeSettings(config, subject);
12562
- const tmpDir = settings?.tmpDir;
13725
+ const tmpDir = agentSettings(config, subject)?.settings.tmpDir;
12563
13726
  return typeof tmpDir === "string" && tmpDir.length > 0 ? (0, apply_1.expandHome)(tmpDir) : osTmpDir;
12564
13727
  }
12565
13728
  async function withConfig(name, probes, body) {
@@ -12585,13 +13748,13 @@ var require_checks = __commonJS({
12585
13748
  };
12586
13749
  }
12587
13750
  function initRunRoot(plan, subject, osTmpDir) {
12588
- if (plan.executor !== WORKSPACE_EXECUTOR)
13751
+ if (plan.executor === void 0 || !AGENT_EXECUTORS2.includes(plan.executor))
12589
13752
  return osTmpDir;
12590
13753
  if (subject.requestedTmpDir !== void 0 && subject.requestedTmpDir.length > 0) {
12591
13754
  return (0, apply_1.expandHome)(subject.requestedTmpDir);
12592
13755
  }
12593
13756
  const settings = {
12594
- ...(0, environment_1.executorDefaultsFor)(subject.brand, subject.brand.keepRegistration.jobsApiUrl)?.[WORKSPACE_EXECUTOR] ?? {},
13757
+ ...(0, environment_1.executorDefaultsFor)(subject.brand, subject.brand.keepRegistration.jobsApiUrl)?.[plan.executor] ?? {},
12595
13758
  ...plan.keptBlock ?? {}
12596
13759
  };
12597
13760
  const tmpDir = settings.tmpDir;
@@ -12787,7 +13950,7 @@ var require_checks = __commonJS({
12787
13950
  }
12788
13951
  const covering = built.filter((e) => e.strategies.includes(loom_1.PROMPT_EXECUTION_STRATEGY));
12789
13952
  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}`);
13953
+ 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
13954
  }
12792
13955
  return pass("executor-coverage", `${covering.map((e) => e.capability.id).join(", ")} handle${covering.length === 1 ? "s" : ""} ${loom_1.PROMPT_EXECUTION_STRATEGY}`);
12793
13956
  });
@@ -12798,13 +13961,13 @@ var require_checks = __commonJS({
12798
13961
  phases: ["init", "run", "doctor"],
12799
13962
  async run(probes, subject) {
12800
13963
  if (subject.phase === "init") {
12801
- return atInit("git-cli", probes, subject, () => toolVerdict("git-cli", "git", probes, subject, {
13964
+ return atInit("git-cli", AGENT_EXECUTORS2, probes, subject, () => toolVerdict("git-cli", "git", probes, subject, {
12802
13965
  missingRemedy: "install git, or configure an executor that needs no checkout"
12803
13966
  }));
12804
13967
  }
12805
13968
  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`);
13969
+ if (activeAgent(config) === null) {
13970
+ return skip("git-cli", "no agent executor configured; nothing checks out a workspace");
12808
13971
  }
12809
13972
  return toolVerdict("git-cli", "git", probes, subject, {
12810
13973
  missingRemedy: "install git, or configure an executor that needs no checkout"
@@ -12812,28 +13975,32 @@ var require_checks = __commonJS({
12812
13975
  });
12813
13976
  }
12814
13977
  };
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`);
13978
+ function agentCli(agent) {
13979
+ const name = `${agent.bin}-cli`;
13980
+ return {
13981
+ name,
13982
+ phases: ["init", "run", "doctor"],
13983
+ async run(probes, subject) {
13984
+ if (subject.phase === "init") {
13985
+ return atInit(name, [agent.executor], probes, subject, () => toolVerdict(name, agent.bin, probes, subject, { missingRemedy: agent.install }));
12825
13986
  }
12826
- return toolVerdict("claude-cli", "claude", probes, subject, { missingRemedy: CLAUDE_INSTALL });
12827
- });
12828
- }
12829
- };
13987
+ return withConfig(name, probes, async (config) => {
13988
+ if (!configuredExecutors(config).includes(agent.executor)) {
13989
+ return skip(name, `no ${agent.executor} executor configured`);
13990
+ }
13991
+ return toolVerdict(name, agent.bin, probes, subject, { missingRemedy: agent.install });
13992
+ });
13993
+ }
13994
+ };
13995
+ }
13996
+ var [CLAUDE, CODEX, COPILOT] = AGENTS;
12830
13997
  var claudeAuth = {
12831
13998
  name: "claude-auth",
12832
13999
  phases: ["run", "doctor"],
12833
14000
  async run(probes, subject) {
12834
14001
  return withConfig("claude-auth", probes, async (config) => {
12835
- if (!configuredExecutors(config).includes(WORKSPACE_EXECUTOR)) {
12836
- return skip("claude-auth", `no ${WORKSPACE_EXECUTOR} executor configured`);
14002
+ if (!configuredExecutors(config).includes(CLAUDE.executor)) {
14003
+ return skip("claude-auth", `no ${CLAUDE.executor} executor configured`);
12837
14004
  }
12838
14005
  if (config.credentials !== void 0) {
12839
14006
  return skip("claude-auth", "hosted mode: the model key is revealed per job, not read from a login");
@@ -12867,6 +14034,64 @@ var require_checks = __commonJS({
12867
14034
  });
12868
14035
  }
12869
14036
  };
14037
+ var codexAuth = {
14038
+ name: "codex-auth",
14039
+ phases: ["run", "doctor"],
14040
+ async run(probes, subject) {
14041
+ return withConfig("codex-auth", probes, async (config) => {
14042
+ const gate = await agentAuthGate("codex-auth", CODEX, config, probes);
14043
+ if (gate)
14044
+ return gate;
14045
+ if (subject.deep)
14046
+ return roundTripVerdict("codex-auth", CODEX, probes, "run `codex login`");
14047
+ const status = await probes.codexLoginStatus();
14048
+ if (status === null) {
14049
+ return warn("codex-auth", "`codex login status` gave no answer", "run `codex login status` to see why, or doctor --deep");
14050
+ }
14051
+ if (!status.loggedIn) {
14052
+ return fail("codex-auth", "the codex CLI is not signed in", "run `codex login`");
14053
+ }
14054
+ return pass("codex-auth", `${status.detail} (not verified)`);
14055
+ });
14056
+ }
14057
+ };
14058
+ var copilotAuth = {
14059
+ name: "copilot-auth",
14060
+ phases: ["run", "doctor"],
14061
+ async run(probes, subject) {
14062
+ return withConfig("copilot-auth", probes, async (config) => {
14063
+ const gate = await agentAuthGate("copilot-auth", COPILOT, config, probes);
14064
+ if (gate)
14065
+ return gate;
14066
+ if (subject.deep)
14067
+ return roundTripVerdict("copilot-auth", COPILOT, probes, "run `copilot login`");
14068
+ const token = probes.ambientCopilotToken();
14069
+ if (token !== null) {
14070
+ 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`);
14071
+ }
14072
+ return pass("copilot-auth", "no token in the environment; the copilot login is used (not verified; doctor --deep spends one turn to prove it)");
14073
+ });
14074
+ }
14075
+ };
14076
+ async function agentAuthGate(name, agent, config, probes) {
14077
+ if (!configuredExecutors(config).includes(agent.executor)) {
14078
+ return skip(name, `no ${agent.executor} executor configured`);
14079
+ }
14080
+ if (config.credentials !== void 0) {
14081
+ return skip(name, `hosted mode: the ${agent.executor} executor refuses hosted runs`);
14082
+ }
14083
+ if (await probes.which(agent.bin) === null || !(await probes.versionOf(agent.bin)).ran) {
14084
+ return skip(name, `no working ${agent.bin} CLI to be signed in to; see ${agent.bin}-cli`);
14085
+ }
14086
+ return null;
14087
+ }
14088
+ async function roundTripVerdict(name, agent, probes, signIn) {
14089
+ const round = await probes.agentRoundTrip(agent.executor);
14090
+ if (!round.ok) {
14091
+ return fail(name, `the ${agent.bin} CLI refused a one-turn call: ${oneLine(round.error)}`, `${signIn}, then re-run doctor --deep`);
14092
+ }
14093
+ return pass(name, `one-turn call through the ${agent.bin} CLI succeeded`);
14094
+ }
12870
14095
  var runRootWritable = {
12871
14096
  name: "run-root-writable",
12872
14097
  phases: ["init", "run", "doctor"],
@@ -12930,15 +14155,15 @@ var require_checks = __commonJS({
12930
14155
  phases: ["run", "doctor"],
12931
14156
  async run(probes, subject) {
12932
14157
  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`);
14158
+ const agent = agentSettings(config, subject);
14159
+ if (agent === null) {
14160
+ return skip("overlay-hosts", "no agent executor configured");
12936
14161
  }
12937
- const hosts = settings.overlayAllowedHosts;
14162
+ const hosts = agent.settings.overlayAllowedHosts;
12938
14163
  const origins = Array.isArray(hosts) ? hosts.filter((h) => typeof h === "string") : [];
12939
14164
  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)})`);
14165
+ const fallback = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[agent.executor]?.overlayAllowedHosts;
14166
+ 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
14167
  }
12943
14168
  return pass("overlay-hosts", origins.join(", "));
12944
14169
  });
@@ -12969,8 +14194,12 @@ var require_checks = __commonJS({
12969
14194
  needsFetch(keepReachable),
12970
14195
  executorCoverage,
12971
14196
  gitCli,
12972
- claudeCli,
14197
+ agentCli(CLAUDE),
12973
14198
  claudeAuth,
14199
+ agentCli(CODEX),
14200
+ codexAuth,
14201
+ agentCli(COPILOT),
14202
+ copilotAuth,
12974
14203
  runRootWritable,
12975
14204
  diskSpace,
12976
14205
  overlayHosts,
@@ -13002,13 +14231,12 @@ var require_checks = __commonJS({
13002
14231
  }
13003
14232
  return `run \`${bin} --version\` to see why ${found} fails, then reinstall it (${missingRemedy})`;
13004
14233
  }
13005
- var CLAUDE_INSTALL = "install Claude Code (npm install -g @anthropic-ai/claude-code)";
13006
- async function atInit(name, probes, subject, body) {
14234
+ async function atInit(name, needs, probes, subject, body) {
13007
14235
  const plan = initPlan(probes, subject);
13008
14236
  if (!plan.ok)
13009
14237
  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`);
14238
+ if (plan.value.executor === void 0 || !needs.includes(plan.value.executor)) {
14239
+ 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
14240
  }
13013
14241
  return body();
13014
14242
  }
@@ -21233,7 +22461,7 @@ var require_schema4 = __commonJS({
21233
22461
  message: 'Each pool must specify at least one of "id" or "name"'
21234
22462
  });
21235
22463
  var executorSchema = zod_1.z.object({
21236
- type: zod_1.z.enum(["claude-code", "http-api", "anthropic-api", "webhook", "custom-script"])
22464
+ type: zod_1.z.enum(["claude-code", "codex", "copilot", "http-api", "anthropic-api", "webhook", "custom-script"])
21237
22465
  }).passthrough();
21238
22466
  var shuttleConfigSchema = zod_1.z.object({
21239
22467
  auth: zod_1.z.object({
@@ -21483,11 +22711,14 @@ var require_probes = __commonJS({
21483
22711
  var jobs_1 = require_dist4();
21484
22712
  var scaffold_1 = require_scaffold();
21485
22713
  var loader_1 = require_loader();
22714
+ var codex_1 = require_codex();
22715
+ var copilot_1 = require_copilot();
21486
22716
  var store_1 = require_store();
21487
22717
  var version_check_1 = require_version_check();
21488
22718
  var PROBE_TIMEOUT_MS = 1e4;
21489
22719
  var VERSION_TIMEOUT_MS = 1e4;
21490
22720
  var DEEP_PROBE_PROMPT = "Reply with the single word: ok";
22721
+ var COPILOT_TOKEN_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
21491
22722
  function createProbes(options) {
21492
22723
  let observedLatest = null;
21493
22724
  const observe = (response) => {
@@ -21708,6 +22939,24 @@ var require_probes = __commonJS({
21708
22939
  }
21709
22940
  return { ok: true, value: void 0 };
21710
22941
  },
22942
+ async codexLoginStatus() {
22943
+ const outcome = await runCapturing("codex", ["login", "status"], VERSION_TIMEOUT_MS);
22944
+ if (outcome.ok) {
22945
+ const line = `${outcome.stdout}
22946
+ ${outcome.stderr}`.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
22947
+ return { loggedIn: true, detail: line ?? "signed in" };
22948
+ }
22949
+ if (outcome.exited && /not logged in/i.test(outcome.reason)) {
22950
+ return { loggedIn: false, detail: "not logged in" };
22951
+ }
22952
+ return null;
22953
+ },
22954
+ ambientCopilotToken: () => COPILOT_TOKEN_VARS.find((name) => (process.env[name] ?? "").length > 0) ?? null,
22955
+ async agentRoundTrip(agent) {
22956
+ const executor = agent === "codex" ? (0, codex_1.createCodexExecutor)({}) : (0, copilot_1.createCopilotExecutor)({});
22957
+ const readiness = await executor.checkReadiness(AbortSignal.timeout(18e4));
22958
+ return readiness.ready ? { ok: true, value: void 0 } : { ok: false, error: readiness.reason };
22959
+ },
21711
22960
  latestVersion: () => observedLatest
21712
22961
  };
21713
22962
  }
@@ -21774,7 +23023,7 @@ var require_probes = __commonJS({
21774
23023
  child.on("close", (code, signal) => {
21775
23024
  clearTimeout(timer);
21776
23025
  if (code === 0) {
21777
- settle({ ok: true, stdout: stdout.trim() });
23026
+ settle({ ok: true, stdout: stdout.trim(), stderr: stderr.trim() });
21778
23027
  return;
21779
23028
  }
21780
23029
  const status = code === null ? `was killed by ${signal ?? "a signal"}` : `exited ${code}`;
@@ -25380,6 +26629,37 @@ var require_logger = __commonJS({
25380
26629
  }
25381
26630
  });
25382
26631
 
26632
+ // ../../packages/shuttle/dist/core/demux.js
26633
+ var require_demux = __commonJS({
26634
+ "../../packages/shuttle/dist/core/demux.js"(exports2) {
26635
+ "use strict";
26636
+ Object.defineProperty(exports2, "__esModule", { value: true });
26637
+ exports2.createDemux = createDemux;
26638
+ function createDemux(executors, pools) {
26639
+ const byStrategy = /* @__PURE__ */ new Map();
26640
+ for (const executor of executors) {
26641
+ for (const strategy of executor.strategies)
26642
+ byStrategy.set(strategy, executor);
26643
+ }
26644
+ const byName = new Map(executors.map((e) => [e.capability.id, e]));
26645
+ const byPool = /* @__PURE__ */ new Map();
26646
+ for (const pool of pools) {
26647
+ const named = byName.get(pool.executor);
26648
+ if (!named)
26649
+ continue;
26650
+ if (pool.id)
26651
+ byPool.set(`id:${pool.id}`, named);
26652
+ if (pool.name)
26653
+ byPool.set(`name:${pool.name}`, named);
26654
+ }
26655
+ return (pool, strategy) => {
26656
+ const named = byPool.get(`id:${pool.id}`) ?? byPool.get(`name:${pool.name}`);
26657
+ return named && named.strategies.includes(strategy) ? named : byStrategy.get(strategy);
26658
+ };
26659
+ }
26660
+ }
26661
+ });
26662
+
25383
26663
  // ../../packages/shuttle/dist/observers/logger.js
25384
26664
  var require_logger2 = __commonJS({
25385
26665
  "../../packages/shuttle/dist/observers/logger.js"(exports2) {
@@ -25901,6 +27181,9 @@ var require_shuttle = __commonJS({
25901
27181
  var logger_1 = require_logger();
25902
27182
  var registry_1 = require_registry();
25903
27183
  var claude_code_1 = require_claude_code();
27184
+ var demux_1 = require_demux();
27185
+ var codex_1 = require_codex();
27186
+ var copilot_1 = require_copilot();
25904
27187
  var http_api_1 = require_http_api();
25905
27188
  var anthropic_api_1 = require_anthropic_api();
25906
27189
  var webhook_1 = require_webhook();
@@ -26098,11 +27381,7 @@ var require_shuttle = __commonJS({
26098
27381
  const registry = new registry_1.ExecutorRegistry(this.brand.executorAllowlist);
26099
27382
  registerBuiltInExecutors(registry, this.brand.executorAllowlist);
26100
27383
  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
- }
27384
+ const executorFor = (0, demux_1.createDemux)(executors, this.config.pools);
26106
27385
  this.spendTracker = (0, spend_tracker_1.createSpendTrackerObserver)(this.brand, paths.spendStateFile);
26107
27386
  const observers = [
26108
27387
  (0, logger_2.createLoggerObserver)(),
@@ -26123,7 +27402,7 @@ var require_shuttle = __commonJS({
26123
27402
  const attendanceId = dispatch.attendanceId;
26124
27403
  const startedAt = Date.now();
26125
27404
  const strategy = (0, loom_1.readShuttleStrategy)(dispatch.inputs) ?? "";
26126
- const executor = byStrategy.get(strategy);
27405
+ const executor = executorFor(dispatch.pool, strategy);
26127
27406
  if (!executor) {
26128
27407
  const reason = `no executor for shuttle strategy "${strategy}"`;
26129
27408
  await observerChain.notify({ kind: "attendance.closed", jobId, attendanceId, status: "failed", reason, totalDurationMs: Date.now() - startedAt }, onObserverError);
@@ -26378,6 +27657,8 @@ var require_shuttle = __commonJS({
26378
27657
  function registerBuiltInExecutors(registry, allowlist) {
26379
27658
  const all = [
26380
27659
  ["claude-code", claude_code_1.createClaudeCodeExecutor],
27660
+ ["codex", codex_1.createCodexExecutor],
27661
+ ["copilot", copilot_1.createCopilotExecutor],
26381
27662
  ["http-api", http_api_1.createHttpApiExecutor],
26382
27663
  ["anthropic-api", anthropic_api_1.createAnthropicApiExecutor],
26383
27664
  ["webhook", webhook_1.createWebhookExecutor],
@@ -26880,8 +28161,8 @@ var require_dist5 = __commonJS({
26880
28161
  "../../packages/shuttle/dist/index.js"(exports2) {
26881
28162
  "use strict";
26882
28163
  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;
28164
+ 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;
28165
+ exports2.revokePairing = exports2.listPairings = exports2.PairingConfigStore = exports2.pair = exports2.scaffoldedConfigPath = exports2.planScaffold = exports2.DEFAULT_REFUSAL_MESSAGE = exports2.DEFAULT_MONTHLY_PERIOD = void 0;
26885
28166
  var cli_1 = require_cli();
26886
28167
  Object.defineProperty(exports2, "createCli", { enumerable: true, get: function() {
26887
28168
  return cli_1.createCli;
@@ -26960,6 +28241,14 @@ var require_dist5 = __commonJS({
26960
28241
  Object.defineProperty(exports2, "createClaudeCodeExecutor", { enumerable: true, get: function() {
26961
28242
  return claude_code_1.createClaudeCodeExecutor;
26962
28243
  } });
28244
+ var codex_1 = require_codex();
28245
+ Object.defineProperty(exports2, "createCodexExecutor", { enumerable: true, get: function() {
28246
+ return codex_1.createCodexExecutor;
28247
+ } });
28248
+ var copilot_1 = require_copilot();
28249
+ Object.defineProperty(exports2, "createCopilotExecutor", { enumerable: true, get: function() {
28250
+ return copilot_1.createCopilotExecutor;
28251
+ } });
26963
28252
  var http_api_1 = require_http_api();
26964
28253
  Object.defineProperty(exports2, "createHttpApiExecutor", { enumerable: true, get: function() {
26965
28254
  return http_api_1.createHttpApiExecutor;
@@ -27086,7 +28375,8 @@ var import_path = require("path");
27086
28375
  var import_promises = require("fs/promises");
27087
28376
  var import_yaml = __toESM(require_dist3());
27088
28377
  var import_shuttle = __toESM(require_dist5());
27089
- var buildVersion = true ? "0.30.23" : pkg.version;
28378
+ var buildVersion = true ? "0.31.0" : pkg.version;
28379
+ var AGENT_EXECUTORS = ["claude-code", "codex", "copilot"];
27090
28380
  var sifterBrand = {
27091
28381
  product: {
27092
28382
  id: "sifter",
@@ -27109,7 +28399,7 @@ var sifterBrand = {
27109
28399
  // shared config across multiple machines.
27110
28400
  configSearchPaths: ["~/.sifter/sifter.yaml"]
27111
28401
  },
27112
- executorAllowlist: ["claude-code", "anthropic-api", "http-api", "webhook", "custom-script"],
28402
+ executorAllowlist: ["claude-code", "codex", "copilot", "anthropic-api", "http-api", "webhook", "custom-script"],
27113
28403
  // What `init` configures when neither --executor nor an existing
27114
28404
  // sifter.yaml names one. The init pre-flight reads it too, to check git and
27115
28405
  // claude before the pairing code is spent.
@@ -27123,23 +28413,35 @@ var sifterBrand = {
27123
28413
  // newest of the flags every review passes (`--tools` arrived in 2.0.31;
27124
28414
  // the executor's REVIEW_RUN_ARGS names them all), and the hosted image pins
27125
28415
  // 2.1.x. Below it a review fails on an unknown option.
28416
+ //
28417
+ // `codex` and `copilot` are floored at the releases their containment was
28418
+ // verified against, and the floor is a containment rule rather than a
28419
+ // compatibility one: an older codex that does not know permission profiles
28420
+ // ignores the review's profile and falls back to a sandbox that reads the
28421
+ // whole disk, and an older copilot may not know a flag that keeps a tool or
28422
+ // a hook out of the run. Both executors are local only.
27126
28423
  minimumToolVersions: {
27127
28424
  git: "2.20.0",
27128
- claude: "2.0.45"
28425
+ claude: "2.0.45",
28426
+ codex: "0.162.0",
28427
+ copilot: "1.0.97"
27129
28428
  },
27130
28429
  // The overlay-fetch allowlist is NOT here: each tier signs its standards
27131
28430
  // URLs with its own Sift origin (SIFT_PUBLIC_API_URL), so the origin is a
27132
28431
  // property of the tier and lives on its `environments` entry below. A
27133
28432
  // 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
- },
28433
+ executorDefaults: Object.fromEntries(
28434
+ AGENT_EXECUTORS.map((executor) => [
28435
+ executor,
28436
+ {
28437
+ // Transcript retention is a diagnostic switch a person flips for one
28438
+ // session, so it rides the environment rather than a file they would
28439
+ // have to remember to unset. The shared package reads no env of its
28440
+ // own (BE-005); this brand is where the variable becomes config.
28441
+ ...process.env.SHUTTLE_KEEP_TRANSCRIPTS ? { keepTranscripts: process.env.SHUTTLE_KEEP_TRANSCRIPTS } : {}
28442
+ }
28443
+ ])
28444
+ ),
27143
28445
  // The environments Whittle Labs operates, keyed by the profile name that
27144
28446
  // selects one. `init --profile stage` resolves both URLs from here, so a
27145
28447
  // non-prod pairing is one flag instead of three and there is no host to
@@ -27244,7 +28546,7 @@ var sifterBrand = {
27244
28546
  }
27245
28547
  const executorBlock = plan.keptBlock !== void 0 ? {
27246
28548
  ...plan.keptBlock,
27247
- ...executor === "claude-code" && ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
28549
+ ...AGENT_EXECUTORS.includes(executor) && ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
27248
28550
  } : await buildExecutorBlock(executor, ctx);
27249
28551
  const doc = {
27250
28552
  auth: {
@@ -27278,13 +28580,15 @@ var sifterBrand = {
27278
28580
  ]
27279
28581
  };
27280
28582
  function overlayOrigin(origin) {
27281
- return { "claude-code": { overlayAllowedHosts: [origin] } };
28583
+ return Object.fromEntries(AGENT_EXECUTORS.map((e) => [e, { overlayAllowedHosts: [origin] }]));
27282
28584
  }
27283
28585
  async function buildExecutorBlock(executor, ctx) {
27284
28586
  switch (executor) {
27285
- case "claude-code": {
28587
+ case "claude-code":
28588
+ case "codex":
28589
+ case "copilot": {
27286
28590
  return {
27287
- type: "claude-code",
28591
+ type: executor,
27288
28592
  ...ctx.setupOptions.tmpDir ? { tmpDir: ctx.setupOptions.tmpDir } : {}
27289
28593
  };
27290
28594
  }