@rallycry/conveyor-agent 10.13.65 → 10.13.66

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 (41) hide show
  1. package/dist/{boot-7OPX55AO.js → boot-UUPTBQ6R.js} +7 -8
  2. package/dist/{chunk-JIGG755T.js → chunk-6Q6LQBWO.js} +0 -1
  3. package/dist/{chunk-ULS4QPRE.js → chunk-6W6UZ4SJ.js} +0 -1
  4. package/dist/{chunk-BAQ2OAKB.js → chunk-75FUOUYX.js} +514 -213
  5. package/dist/{chunk-AXA55U4Z.js → chunk-E2SHIH6Y.js} +3 -4
  6. package/dist/{chunk-QOJTJCYZ.js → chunk-EXQ6AHOY.js} +2 -3
  7. package/dist/{chunk-5OQQSDVT.js → chunk-IA45XHOA.js} +0 -1
  8. package/dist/{chunk-QZN6HVUY.js → chunk-KG4ORL3Y.js} +1 -2
  9. package/dist/{chunk-4VUQ2NPF.js → chunk-KMB3BU4S.js} +0 -1
  10. package/dist/{chunk-NAK6FY5U.js → chunk-LE6ZUDZT.js} +37 -6
  11. package/dist/{chunk-6GS5ADIY.js → chunk-R3FDJQL6.js} +0 -1
  12. package/dist/cli.js +35 -43
  13. package/dist/{client-LRVVHTNG.js → client-IG6C5F2G.js} +3 -4
  14. package/dist/heartbeat-worker.js +1 -2
  15. package/dist/index.d.ts +95 -13
  16. package/dist/index.js +7 -8
  17. package/dist/{mode-6RL3SVMV.js → mode-ZJSOSLGU.js} +1 -2
  18. package/dist/{oom-watchdog-U7JERHA2.js → oom-watchdog-PAC5OJJG.js} +1 -2
  19. package/dist/{protocol-QLVS5W6O.js → protocol-QBCYO4GI.js} +1 -2
  20. package/dist/server-US2DDQSW.js +9 -0
  21. package/package.json +1 -1
  22. package/dist/boot-7OPX55AO.js.map +0 -1
  23. package/dist/chunk-4VUQ2NPF.js.map +0 -1
  24. package/dist/chunk-5OQQSDVT.js.map +0 -1
  25. package/dist/chunk-6GS5ADIY.js.map +0 -1
  26. package/dist/chunk-AXA55U4Z.js.map +0 -1
  27. package/dist/chunk-BAQ2OAKB.js.map +0 -1
  28. package/dist/chunk-JIGG755T.js.map +0 -1
  29. package/dist/chunk-NAK6FY5U.js.map +0 -1
  30. package/dist/chunk-QOJTJCYZ.js.map +0 -1
  31. package/dist/chunk-QZN6HVUY.js.map +0 -1
  32. package/dist/chunk-ULS4QPRE.js.map +0 -1
  33. package/dist/cli.js.map +0 -1
  34. package/dist/client-LRVVHTNG.js.map +0 -1
  35. package/dist/heartbeat-worker.js.map +0 -1
  36. package/dist/index.js.map +0 -1
  37. package/dist/mode-6RL3SVMV.js.map +0 -1
  38. package/dist/oom-watchdog-U7JERHA2.js.map +0 -1
  39. package/dist/protocol-QLVS5W6O.js.map +0 -1
  40. package/dist/server-ZACB5T5S.js +0 -10
  41. package/dist/server-ZACB5T5S.js.map +0 -1
@@ -7,7 +7,7 @@ import {
7
7
  readAgentVersion,
8
8
  registerBootMilestoneSocketFallback,
9
9
  reportBootMilestone
10
- } from "./chunk-QZN6HVUY.js";
10
+ } from "./chunk-KG4ORL3Y.js";
11
11
  import {
12
12
  LoopLagMonitor,
13
13
  buildConveyorSocketOptions,
@@ -15,14 +15,14 @@ import {
15
15
  heartbeatStatusFor,
16
16
  loopStatusForRunnerStatus,
17
17
  waitForConnected
18
- } from "./chunk-5OQQSDVT.js";
18
+ } from "./chunk-IA45XHOA.js";
19
19
  import {
20
20
  WorkbenchError,
21
21
  getWorkbenchClient
22
- } from "./chunk-QOJTJCYZ.js";
22
+ } from "./chunk-EXQ6AHOY.js";
23
23
  import {
24
24
  workbenchEnabled
25
- } from "./chunk-4VUQ2NPF.js";
25
+ } from "./chunk-KMB3BU4S.js";
26
26
  import {
27
27
  MAX_BETWEEN_TURN_BUFFER,
28
28
  MAX_DIAGNOSTIC_OUTPUT,
@@ -30,6 +30,7 @@ import {
30
30
  buildPromptBytes,
31
31
  buildSpawnArgs,
32
32
  cleanTerminalOutput,
33
+ describeTokenFile,
33
34
  ghHostsExternallyOwned,
34
35
  gitCredentialHelper,
35
36
  githubTokenFilePath,
@@ -54,7 +55,7 @@ import {
54
55
  transcriptSize,
55
56
  turnOptionsFrom,
56
57
  writeGitCredential
57
- } from "./chunk-NAK6FY5U.js";
58
+ } from "./chunk-LE6ZUDZT.js";
58
59
 
59
60
  // src/setup/bootstrap.ts
60
61
  var BOOTSTRAP_TIMEOUT_MS = 3e4;
@@ -73,11 +74,11 @@ async function singleBootstrapAttempt(apiUrl, instanceName, bootstrapToken, time
73
74
  signal: controller.signal
74
75
  });
75
76
  if (!response.ok) {
76
- const errorText = await response.text().catch(() => "");
77
+ const errorText2 = await response.text().catch(() => "");
77
78
  return {
78
79
  ok: false,
79
80
  status: response.status,
80
- errorText: errorText.slice(0, 500),
81
+ errorText: errorText2.slice(0, 500),
81
82
  reason: response.status === 401 || response.status === 403 ? "auth_rejected" : "http_error"
82
83
  };
83
84
  }
@@ -167,6 +168,28 @@ function applyBootstrapToEnv(config) {
167
168
  if (config.taskBranch) process.env.CONVEYOR_TASK_BRANCH = config.taskBranch;
168
169
  }
169
170
 
171
+ // src/utils/logger.ts
172
+ function createServiceLogger(service) {
173
+ const prefix = `[conveyor-agent:${service}]`;
174
+ return {
175
+ info(message, data) {
176
+ const extra = data ? ` ${JSON.stringify(data)}` : "";
177
+ process.stderr.write(`${prefix} ${message}${extra}
178
+ `);
179
+ },
180
+ warn(message, data) {
181
+ const extra = data ? ` ${JSON.stringify(data)}` : "";
182
+ process.stderr.write(`${prefix} WARN ${message}${extra}
183
+ `);
184
+ },
185
+ error(message, data) {
186
+ const extra = data ? ` ${JSON.stringify(data)}` : "";
187
+ process.stderr.write(`${prefix} ERROR ${message}${extra}
188
+ `);
189
+ }
190
+ };
191
+ }
192
+
170
193
  // src/connection/agent-connection.ts
171
194
  import { existsSync } from "fs";
172
195
  import { fileURLToPath } from "url";
@@ -204,7 +227,53 @@ async function pollUntilBound(opts) {
204
227
  }
205
228
  }
206
229
 
230
+ // src/connection/bundle-credentials.ts
231
+ function readBundleIdentity(sessionJwt) {
232
+ if (!sessionJwt) return {};
233
+ const segments = sessionJwt.split(".");
234
+ if (segments.length !== 3) return {};
235
+ try {
236
+ const json = Buffer.from(segments[1], "base64url").toString("utf8");
237
+ const claims = JSON.parse(json);
238
+ return {
239
+ ...typeof claims.sessionId === "string" ? { sessionId: claims.sessionId } : {},
240
+ ...typeof claims.role === "string" ? { role: claims.role } : {}
241
+ };
242
+ } catch {
243
+ return {};
244
+ }
245
+ }
246
+ function bundleMayWriteGithubFiles(bundle, self) {
247
+ const identity = readBundleIdentity(bundle.sessionJwt);
248
+ if (identity.sessionId && self.sessionId && identity.sessionId !== self.sessionId) {
249
+ return {
250
+ allowed: false,
251
+ reason: `bundle resolved to session ${identity.sessionId}, not ours (${self.sessionId})`
252
+ };
253
+ }
254
+ if (identity.role === "reader" && self.role && self.role !== "reader") {
255
+ return {
256
+ allowed: false,
257
+ reason: `bundle carries a reader-scoped token but this session is a ${self.role}`
258
+ };
259
+ }
260
+ return { allowed: true };
261
+ }
262
+ function applyBundleGithubToken(bundle, self) {
263
+ if (!bundle.githubToken) return { written: false, reason: "bundle carried no GitHub token" };
264
+ const permitted = bundleMayWriteGithubFiles(bundle, self);
265
+ if (!permitted.allowed) {
266
+ return { written: false, ...permitted.reason ? { reason: permitted.reason } : {} };
267
+ }
268
+ syncGithubTokenFiles(bundle.githubToken);
269
+ return { written: true };
270
+ }
271
+ function syncBundleGithubToken(token) {
272
+ syncGithubTokenFiles(token);
273
+ }
274
+
207
275
  // src/connection/agent-connection.ts
276
+ var logger = createServiceLogger("agent-connection");
208
277
  var EVENT_BATCH_MS = 500;
209
278
  var MAX_EVENT_BUFFER = 5e3;
210
279
  var TOKEN_REFRESH_INTERVAL_MS = 45 * 60 * 1e3;
@@ -237,6 +306,7 @@ var AgentConnection = class _AgentConnection {
237
306
  apiKeyUpdateCallback = null;
238
307
  pullBranchCallback = null;
239
308
  runStartCommandCallback = null;
309
+ earlyRunStartCommand = false;
240
310
  earlyPullBranches = [];
241
311
  spawnReviewCallback = null;
242
312
  earlySpawnReviews = [];
@@ -426,7 +496,8 @@ var AgentConnection = class _AgentConnection {
426
496
  else this.earlyProbeUsage = true;
427
497
  });
428
498
  this.socket.on("session:runStartCommand", () => {
429
- this.runStartCommandCallback?.();
499
+ if (this.runStartCommandCallback) this.runStartCommandCallback();
500
+ else this.earlyRunStartCommand = true;
430
501
  });
431
502
  this.socket.on("pty:input", (data) => {
432
503
  if (data.sessionId && data.sessionId !== this.config.sessionId) return;
@@ -728,6 +799,26 @@ var AgentConnection = class _AgentConnection {
728
799
  }).catch(() => {
729
800
  });
730
801
  }
802
+ /**
803
+ * Report that this pod's git credential is dead and refreshing did not fix
804
+ * it (fire-and-forget).
805
+ *
806
+ * Purely diagnostic. Until this existed a pod could lose git entirely and
807
+ * leave no server-side trace at all — the refresh RPC succeeded every time,
808
+ * so the failure was visible only in pod stderr, which is why it took two
809
+ * investigations to attribute. `tokenShape` describes the served credential
810
+ * (length, prefix class, mtime) and NEVER carries its value.
811
+ */
812
+ reportCredentialFailure(details) {
813
+ if (!this.socket) return;
814
+ void this.call("reportCredentialFailure", {
815
+ sessionId: this.config.sessionId,
816
+ ...details.error ? { error: details.error.slice(0, 2e3) } : {},
817
+ ...details.tokenShape ? { tokenShape: details.tokenShape.slice(0, 500) } : {},
818
+ ...details.healed === void 0 ? {} : { healed: details.healed }
819
+ }).catch(() => {
820
+ });
821
+ }
731
822
  /**
732
823
  * Ask the server to destroy and recreate this pod (fire-and-forget). The
733
824
  * agent calls this only when it has proven it cannot recover in place — the
@@ -772,8 +863,15 @@ var AgentConnection = class _AgentConnection {
772
863
  }).catch(() => {
773
864
  });
774
865
  }
866
+ /** Register the restart handler, draining a `session:runStartCommand` that
867
+ * arrived before the supervisor was ready. Collapsed to one drain: two
868
+ * clicks during boot should produce one restart, not two competing ones. */
775
869
  onRunStartCommand(callback) {
776
870
  this.runStartCommandCallback = callback;
871
+ if (this.earlyRunStartCommand) {
872
+ this.earlyRunStartCommand = false;
873
+ callback();
874
+ }
777
875
  }
778
876
  // ── PTY relay (S5 Connected-TUI terminal) ──────────────────────────
779
877
  /**
@@ -1159,11 +1257,20 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1159
1257
  // means the token is consumed / session terminal and retrying won't
1160
1258
  // help. Network/timeout still retry inside fetchBootstrap.
1161
1259
  });
1162
- if (!result.ok) return { refreshedClaude: false, refreshedTaskToken: false };
1260
+ if (!result.ok) {
1261
+ logger.warn("bootstrap refresh abandoned \u2014 pod credentials will go stale", {
1262
+ path: "codespace",
1263
+ reason: result.reason,
1264
+ status: result.status,
1265
+ attempts: result.attempts,
1266
+ detail: result.detail
1267
+ });
1268
+ return { refreshedClaude: false, refreshedTaskToken: false };
1269
+ }
1163
1270
  const previousTaskToken = process.env.CONVEYOR_TASK_TOKEN;
1164
1271
  applyBootstrapToEnv(result.config);
1165
1272
  const env = result.config.envVars ?? {};
1166
- syncGithubTokenFiles(env.CONVEYOR_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN);
1273
+ syncBundleGithubToken(env.CONVEYOR_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN);
1167
1274
  const refreshedTaskToken = result.config.mode !== "project" && Boolean(result.config.taskToken) && result.config.taskToken !== previousTaskToken;
1168
1275
  if (refreshedTaskToken && result.config.taskToken) {
1169
1276
  this.config.taskToken = result.config.taskToken;
@@ -1195,7 +1302,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1195
1302
  }
1196
1303
  if (bundle.githubToken) {
1197
1304
  process.env.CONVEYOR_GITHUB_TOKEN = bundle.githubToken;
1198
- syncGithubTokenFiles(bundle.githubToken);
1305
+ this.applyBundleCredentialFiles(bundle, previousTaskToken);
1199
1306
  }
1200
1307
  if (bundle.anthropicKey) process.env.ANTHROPIC_API_KEY = bundle.anthropicKey;
1201
1308
  if (bundle.gcpToken) process.env.CLOUDSDK_AUTH_ACCESS_TOKEN = bundle.gcpToken;
@@ -1214,6 +1321,32 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1214
1321
  const refreshedClaude = Boolean(bundle.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
1215
1322
  return { refreshedClaude, refreshedTaskToken };
1216
1323
  }
1324
+ /**
1325
+ * Write the bundle's GitHub token to the shared credential files — unless the
1326
+ * bundle belongs to another session.
1327
+ *
1328
+ * The bootstrap GET is keyed by the POD, so on a pod also hosting a same-pod
1329
+ * review it can resolve to the reader session, whose token is read-only.
1330
+ * Writing that over the shared files silently downgrades the builder's push
1331
+ * credential. Our own taskToken carries the same claims, so it is what we
1332
+ * compare against.
1333
+ *
1334
+ * The legitimate case this path exists for — our own session's bundle
1335
+ * refreshing the token when the RPC is failing — is unaffected.
1336
+ */
1337
+ applyBundleCredentialFiles(bundle, previousTaskToken) {
1338
+ const self = readBundleIdentity(previousTaskToken);
1339
+ const result = applyBundleGithubToken(bundle, {
1340
+ sessionId: this.config.sessionId || self.sessionId,
1341
+ ...self.role ? { role: self.role } : {}
1342
+ });
1343
+ if (!result.written && result.reason) {
1344
+ process.stderr.write(
1345
+ `[conveyor-agent] Skipped writing GitHub credential files from the bootstrap bundle: ${result.reason}
1346
+ `
1347
+ );
1348
+ }
1349
+ }
1217
1350
  /**
1218
1351
  * Poll the bootstrap bundle once (maxWaitMs 0), retrying only on a transient
1219
1352
  * 429 (standby-pool pods share one Cloud NAT IP against podBootstrapLimiter)
@@ -1229,6 +1362,12 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1229
1362
  } catch (err) {
1230
1363
  const isRateLimited = err instanceof PollUntilBoundHttpError && err.status === 429;
1231
1364
  if (!isRateLimited || attempt >= retryDelaysMs.length) {
1365
+ logger.warn("bootstrap refresh abandoned \u2014 pod credentials will go stale", {
1366
+ path: "v3",
1367
+ attempt,
1368
+ rateLimited: isRateLimited,
1369
+ error: err instanceof Error ? err.message : String(err)
1370
+ });
1232
1371
  return null;
1233
1372
  }
1234
1373
  await new Promise((resolve) => {
@@ -1290,31 +1429,8 @@ function isPermissionDeniedError(err) {
1290
1429
  return /insufficient permissions|authentication required/i.test(message);
1291
1430
  }
1292
1431
 
1293
- // src/utils/logger.ts
1294
- function createServiceLogger(service) {
1295
- const prefix = `[conveyor-agent:${service}]`;
1296
- return {
1297
- info(message, data) {
1298
- const extra = data ? ` ${JSON.stringify(data)}` : "";
1299
- process.stderr.write(`${prefix} ${message}${extra}
1300
- `);
1301
- },
1302
- warn(message, data) {
1303
- const extra = data ? ` ${JSON.stringify(data)}` : "";
1304
- process.stderr.write(`${prefix} WARN ${message}${extra}
1305
- `);
1306
- },
1307
- error(message, data) {
1308
- const extra = data ? ` ${JSON.stringify(data)}` : "";
1309
- process.stderr.write(`${prefix} ERROR ${message}${extra}
1310
- `);
1311
- }
1312
- };
1313
- }
1314
-
1315
- // src/runner/git-utils.ts
1432
+ // src/runner/git-run.ts
1316
1433
  import { execFile } from "child_process";
1317
- import { realpathSync } from "fs";
1318
1434
  import { promisify } from "util";
1319
1435
  var execFileAsync = promisify(execFile);
1320
1436
  var GIT_TIMEOUT_MS = 6e4;
@@ -1322,12 +1438,19 @@ var GIT_SLOW_TIMEOUT_MS = 12e4;
1322
1438
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
1323
1439
  async function git(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
1324
1440
  if (workbenchEnabled()) {
1325
- const { stdout: stdout2 } = await getWorkbenchClient().execFile("git", args, {
1326
- cwd,
1327
- timeout: timeoutMs,
1328
- maxBuffer: GIT_MAX_BUFFER
1329
- });
1330
- return stdout2.trim();
1441
+ try {
1442
+ const { stdout: stdout2 } = await getWorkbenchClient().execFile("git", args, {
1443
+ cwd,
1444
+ timeout: timeoutMs,
1445
+ maxBuffer: GIT_MAX_BUFFER
1446
+ });
1447
+ return stdout2.trim();
1448
+ } catch (err) {
1449
+ if (err instanceof Error && err.message.startsWith("Command timed out:")) {
1450
+ err.killed = true;
1451
+ }
1452
+ throw err;
1453
+ }
1331
1454
  }
1332
1455
  const { stdout } = await execFileAsync("git", args, {
1333
1456
  cwd,
@@ -1336,6 +1459,54 @@ async function git(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
1336
1459
  });
1337
1460
  return stdout.toString().trim();
1338
1461
  }
1462
+
1463
+ // src/runner/git-credential-ops.ts
1464
+ function credentialErrorText(err) {
1465
+ const raw = err instanceof Error ? err.message : String(err);
1466
+ return raw.replace(/\/\/[^@\s]+@/g, "//***@");
1467
+ }
1468
+ async function updateRemoteCredential(cwd, credential) {
1469
+ const result = { ok: false, storeWritten: false, helperConfigured: false };
1470
+ try {
1471
+ const currentUrl = await git(cwd, ["remote", "get-url", "origin"]);
1472
+ const cloneUrl = credential.cloneUrl ?? currentUrl;
1473
+ const normalizedUrl = writeGitCredential(cwd, cloneUrl, credential);
1474
+ result.storeWritten = true;
1475
+ if (currentUrl !== normalizedUrl) {
1476
+ await git(cwd, ["remote", "set-url", "origin", normalizedUrl]);
1477
+ }
1478
+ await git(cwd, ["config", "--local", "credential.helper", gitCredentialHelper(cwd)]);
1479
+ result.helperConfigured = true;
1480
+ result.ok = true;
1481
+ } catch (err) {
1482
+ result.error = credentialErrorText(err);
1483
+ }
1484
+ return result;
1485
+ }
1486
+ async function updateRemoteToken(cwd, token) {
1487
+ const username = process.env.CONVEYOR_GIT_USERNAME || "x-access-token";
1488
+ const cloneUrl = process.env.CONVEYOR_GIT_CLONE_URL || void 0;
1489
+ const credential = await updateRemoteCredential(cwd, { username, secret: token, cloneUrl });
1490
+ process.env.CONVEYOR_GIT_SECRET = token;
1491
+ const files = syncGithubTokenFiles(token);
1492
+ return { credential, files };
1493
+ }
1494
+ async function verifyGitCredential(cwd) {
1495
+ try {
1496
+ await git(cwd, ["ls-remote", "--heads", "origin"], 3e4);
1497
+ return { ok: true, outcome: "ok" };
1498
+ } catch (err) {
1499
+ const killed = err.killed === true;
1500
+ return {
1501
+ ok: false,
1502
+ outcome: killed ? "timeout" : "denied",
1503
+ error: credentialErrorText(err)
1504
+ };
1505
+ }
1506
+ }
1507
+
1508
+ // src/runner/git-utils.ts
1509
+ import { realpathSync } from "fs";
1339
1510
  async function ensureOnTaskBranch(cwd, taskBranch, baseBranch) {
1340
1511
  if (!taskBranch) return true;
1341
1512
  try {
@@ -1464,44 +1635,6 @@ async function isAuthError(cwd) {
1464
1635
  return errLooksLikeAuth(err);
1465
1636
  }
1466
1637
  }
1467
- function credentialErrorText(err) {
1468
- const raw = err instanceof Error ? err.message : String(err);
1469
- return raw.replace(/\/\/[^@\s]+@/g, "//***@");
1470
- }
1471
- async function updateRemoteCredential(cwd, credential) {
1472
- const result = { ok: false, storeWritten: false, helperConfigured: false };
1473
- try {
1474
- const currentUrl = await git(cwd, ["remote", "get-url", "origin"]);
1475
- const cloneUrl = credential.cloneUrl ?? currentUrl;
1476
- const normalizedUrl = writeGitCredential(cwd, cloneUrl, credential);
1477
- result.storeWritten = true;
1478
- if (currentUrl !== normalizedUrl) {
1479
- await git(cwd, ["remote", "set-url", "origin", normalizedUrl]);
1480
- }
1481
- await git(cwd, ["config", "--local", "credential.helper", gitCredentialHelper(cwd)]);
1482
- result.helperConfigured = true;
1483
- result.ok = true;
1484
- } catch (err) {
1485
- result.error = credentialErrorText(err);
1486
- }
1487
- return result;
1488
- }
1489
- async function updateRemoteToken(cwd, token) {
1490
- const username = process.env.CONVEYOR_GIT_USERNAME || "x-access-token";
1491
- const cloneUrl = process.env.CONVEYOR_GIT_CLONE_URL || void 0;
1492
- const credential = await updateRemoteCredential(cwd, { username, secret: token, cloneUrl });
1493
- process.env.CONVEYOR_GIT_SECRET = token;
1494
- const files = syncGithubTokenFiles(token);
1495
- return { credential, files };
1496
- }
1497
- async function verifyGitCredential(cwd) {
1498
- try {
1499
- await git(cwd, ["ls-remote", "--heads", "origin"], 3e4);
1500
- return { ok: true };
1501
- } catch (err) {
1502
- return { ok: false, error: credentialErrorText(err) };
1503
- }
1504
- }
1505
1638
  function wipRefForBranch(branch) {
1506
1639
  return `conveyor-wip/${branch}`;
1507
1640
  }
@@ -1645,7 +1778,7 @@ async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
1645
1778
  }
1646
1779
  if (await tryPush(cwd, currentBranch, skipVerify)) return true;
1647
1780
  if (refreshToken && await isAuthError(cwd)) {
1648
- const token = await refreshToken();
1781
+ const token = await refreshToken({ forceFresh: true });
1649
1782
  if (token) {
1650
1783
  await updateRemoteToken(cwd, token);
1651
1784
  process.env.GITHUB_TOKEN = token;
@@ -2021,16 +2154,19 @@ var KnownAgentEventSchema = z3.discriminatedUnion("type", [
2021
2154
  cacheCreationInputTokens: z3.number().optional(),
2022
2155
  totalTokensUsed: z3.number().optional()
2023
2156
  }).passthrough(),
2024
- // Three producer shapes share this type: {rateLimitType, utilization, status}
2025
- // (SDK rate_limit_event), {resetsAt} (agent-connection resume notice), and
2026
- // the usage-sampler ({rateLimitType, utilization, status, resetsAt, gauges}
2027
- // — resetsAt matches rateLimitType; gauges survives via .passthrough()).
2157
+ // Four producer shapes share this type: {rateLimitType, utilization, status}
2158
+ // (SDK rate_limit_event), {resetsAt} (agent-connection resume notice), the
2159
+ // usage-sampler ({rateLimitType, utilization, status, resetsAt, gauges}
2160
+ // — resetsAt matches rateLimitType; gauges survives via .passthrough()), and
2161
+ // {unmeasurable, reason} (the sampler reporting it cannot read this key).
2028
2162
  z3.object({
2029
2163
  type: z3.literal("rate_limit_update"),
2030
2164
  rateLimitType: z3.string().optional(),
2031
2165
  utilization: z3.number().optional(),
2032
2166
  status: z3.string().optional(),
2033
- resetsAt: z3.string().optional()
2167
+ resetsAt: z3.string().optional(),
2168
+ unmeasurable: z3.boolean().optional(),
2169
+ reason: z3.string().optional()
2034
2170
  }).passthrough(),
2035
2171
  z3.object({
2036
2172
  type: z3.literal("context_compacted"),
@@ -2074,6 +2210,7 @@ var KnownAgentEventSchema = z3.discriminatedUnion("type", [
2074
2210
  z3.object({
2075
2211
  type: z3.literal("setup_complete"),
2076
2212
  startCommandRunning: z3.boolean().optional(),
2213
+ startCommandConfigured: z3.boolean().optional(),
2077
2214
  // Sanitized server-side by sanitizeSessionPreviewPorts — stays unknown.
2078
2215
  previewPorts: z3.unknown().optional()
2079
2216
  }).passthrough(),
@@ -2481,7 +2618,14 @@ var EmitAgentEventRequestSchema = z4.object({
2481
2618
  events: z4.array(AgentEventSchema).max(500)
2482
2619
  });
2483
2620
  var RefreshGithubTokenRequestSchema = z4.object({
2484
- sessionId: z4.string()
2621
+ sessionId: z4.string(),
2622
+ forceFresh: z4.boolean().optional()
2623
+ });
2624
+ var ReportCredentialFailureRequestSchema = z4.object({
2625
+ sessionId: z4.string(),
2626
+ error: z4.string().max(2e3).optional(),
2627
+ tokenShape: z4.string().max(500).optional(),
2628
+ healed: z4.boolean().optional()
2485
2629
  });
2486
2630
  var ReportReviewSpawnFailureRequestSchema = z4.object({
2487
2631
  sessionId: z4.string(),
@@ -2621,6 +2765,18 @@ var SessionStopResponseSchema = z4.object({
2621
2765
  var DeleteSubtaskResponseSchema = z4.object({
2622
2766
  deleted: z4.boolean()
2623
2767
  });
2768
+ var GIT_BRANCH_NAME_MAX = 255;
2769
+ var GIT_BRANCH_NAME_MESSAGE = "Invalid git branch name \u2014 use only letters, numbers, '.', '_', '/' and '-', starting with a letter or number, with no '..', '@{' or '//', and no trailing '/', '-', '.' or '.lock'";
2770
+ var ALLOWED_REF = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
2771
+ function isValidGitBranchName(name) {
2772
+ if (typeof name !== "string") return false;
2773
+ if (name.length === 0 || name.length > GIT_BRANCH_NAME_MAX) return false;
2774
+ if (!ALLOWED_REF.test(name)) return false;
2775
+ if (name.includes("..") || name.includes("@{") || name.includes("//")) return false;
2776
+ if (name.endsWith("/") || name.endsWith("-")) return false;
2777
+ if (name.endsWith(".") || name.endsWith(".lock")) return false;
2778
+ return true;
2779
+ }
2624
2780
  var cardDescription2 = z5.string().max(CARD_DESCRIPTION_MAX, CARD_DESCRIPTION_LIMIT_MESSAGE).optional();
2625
2781
  var ListAccessibleProjectsRequestSchema = z5.object({
2626
2782
  pageSize: z5.number().int().positive().max(100).optional().default(100)
@@ -2758,11 +2914,17 @@ var UpdateProjectTaskRequestSchema = z5.object({
2758
2914
  // Move to a different sub-project board, or null to move to the parent board.
2759
2915
  // Validated to belong to `projectId` in the handler.
2760
2916
  subProjectId: z5.string().nullable().optional(),
2917
+ // Record the task's ACTUAL working branch (e.g. a locally-driven pack's
2918
+ // branch, so identification never mints a competing name and pack-child
2919
+ // merge handling matches reality), or null to detach. Guarded in the
2920
+ // handler: the ref must exist on origin and no live workspace may be bound
2921
+ // to a different branch.
2922
+ githubBranch: z5.string().min(1).max(GIT_BRANCH_NAME_MAX).refine(isValidGitBranchName, { message: GIT_BRANCH_NAME_MESSAGE }).nullable().optional(),
2761
2923
  requestingUserId: z5.string().optional()
2762
2924
  }).strict().refine(
2763
- (v) => v.title !== void 0 || v.description !== void 0 || v.plan !== void 0 || v.status !== void 0 || v.risk !== void 0 || v.storyPointValue !== void 0 || v.assignedUserId !== void 0 || v.subProjectId !== void 0,
2925
+ (v) => v.title !== void 0 || v.description !== void 0 || v.plan !== void 0 || v.status !== void 0 || v.risk !== void 0 || v.storyPointValue !== void 0 || v.assignedUserId !== void 0 || v.subProjectId !== void 0 || v.githubBranch !== void 0,
2764
2926
  {
2765
- message: "update_task requires at least one field to change (title, description, plan, status, risk, storyPointValue, assignedUserId, or subProjectId)"
2927
+ message: "update_task requires at least one field to change (title, description, plan, status, risk, storyPointValue, assignedUserId, subProjectId, or githubBranch)"
2766
2928
  }
2767
2929
  );
2768
2930
  var TransitionProjectTaskStatusRequestSchema = z5.object({
@@ -3306,7 +3468,7 @@ var AGENT_CHAT_HISTORY_FETCH_LIMIT = Math.max(TASK_CHAT_HISTORY_LIMIT, PM_CHAT_H
3306
3468
  function formatModelId(provider, model) {
3307
3469
  return `${provider}/${model}`;
3308
3470
  }
3309
- function anthropicEntry(model, label, inputPerMillion, outputPerMillion, experimental = false) {
3471
+ function anthropicEntry(model, label, inputPerMillion, outputPerMillion, opts = {}) {
3310
3472
  return {
3311
3473
  provider: "anthropic",
3312
3474
  model,
@@ -3316,7 +3478,8 @@ function anthropicEntry(model, label, inputPerMillion, outputPerMillion, experim
3316
3478
  inputPrice: inputPerMillion / 1e6,
3317
3479
  outputPrice: outputPerMillion / 1e6,
3318
3480
  supportsTools: true,
3319
- ...experimental ? { experimental: true } : {}
3481
+ supportsEffort: opts.supportsEffort ?? true,
3482
+ ...opts.experimental ? { experimental: true } : {}
3320
3483
  };
3321
3484
  }
3322
3485
  var ANTHROPIC_CATALOG = [
@@ -3324,8 +3487,9 @@ var ANTHROPIC_CATALOG = [
3324
3487
  anthropicEntry(PREVIOUS_OPUS_MODEL, "Opus 4.8", 5, 25),
3325
3488
  anthropicEntry(DEFAULT_SONNET_MODEL, "Sonnet 5 Latest", 3, 15),
3326
3489
  anthropicEntry(PREVIOUS_SONNET_MODEL, "Sonnet 4.6", 3, 15),
3327
- anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5),
3328
- anthropicEntry(FABLE_MODEL, "Fable 5 (experimental)", 10, 50, true)
3490
+ // The Haiku line (4.5 and older) predates the tuning surface and 400s on it.
3491
+ anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
3492
+ anthropicEntry(FABLE_MODEL, "Fable 5 (experimental)", 10, 50, { experimental: true })
3329
3493
  ];
3330
3494
  var HUMAN_PROSE_WRITING_STYLE = `## Writing style for humans
3331
3495
  When you write prose a person will read \u2014 chat messages, plan updates, PR titles and bodies, PR review guides (the \`publish_review_guide\` overview and section explanations), review comments \u2014 follow these rules (based on ASD-STE100 Simplified Technical English):
@@ -7378,7 +7542,7 @@ function findOnPath(binary, env = process.env) {
7378
7542
  import { promises as fs } from "fs";
7379
7543
  import { dirname as dirname2, join as join9 } from "path";
7380
7544
  import { homedir as homedir4 } from "os";
7381
- var logger = createServiceLogger("opencode-auth");
7545
+ var logger2 = createServiceLogger("opencode-auth");
7382
7546
  var OPENCODE_CODEX_PLUGIN = "opencode-openai-codex-auth@4.4.0";
7383
7547
  var PLUGIN_PACKAGE = "opencode-openai-codex-auth";
7384
7548
  function opencodeAuthPath(env) {
@@ -7425,7 +7589,7 @@ async function ensureAuthEntry(env, seed) {
7425
7589
  const path2 = opencodeAuthPath(env);
7426
7590
  const store = await readJsonFile(path2);
7427
7591
  if (!shouldSeed(store.openai, seed)) {
7428
- logger.info("opencode oauth store is fresher than the seed; leaving it alone");
7592
+ logger2.info("opencode oauth store is fresher than the seed; leaving it alone");
7429
7593
  return;
7430
7594
  }
7431
7595
  store.openai = {
@@ -7435,7 +7599,7 @@ async function ensureAuthEntry(env, seed) {
7435
7599
  expires: seed.expires
7436
7600
  };
7437
7601
  await writeJsonFile(path2, store);
7438
- logger.info("seeded opencode oauth store entry");
7602
+ logger2.info("seeded opencode oauth store entry");
7439
7603
  }
7440
7604
  async function ensurePluginConfig(env) {
7441
7605
  const path2 = opencodeConfigPath(env);
@@ -7448,7 +7612,7 @@ async function ensurePluginConfig(env) {
7448
7612
  const kept = plugins.filter((p) => !isOurs(p));
7449
7613
  config.plugin = [...kept, OPENCODE_CODEX_PLUGIN];
7450
7614
  await writeJsonFile(path2, config);
7451
- logger.info("ensured opencode codex-auth plugin in config");
7615
+ logger2.info("ensured opencode codex-auth plugin in config");
7452
7616
  }
7453
7617
  async function seedOpenCodeOauth(env) {
7454
7618
  const seed = parseOauthSeed(env.CONVEYOR_OPENCODE_OAUTH);
@@ -7457,7 +7621,7 @@ async function seedOpenCodeOauth(env) {
7457
7621
  await ensureAuthEntry(env, seed);
7458
7622
  await ensurePluginConfig(env);
7459
7623
  } catch (err) {
7460
- logger.warn(
7624
+ logger2.warn(
7461
7625
  `failed to seed opencode oauth store: ${err instanceof Error ? err.message : String(err)}`
7462
7626
  );
7463
7627
  }
@@ -7826,14 +7990,14 @@ function resolveTuiAdapter(kind = "claude-code") {
7826
7990
 
7827
7991
  // src/harness/pty/stream-server.ts
7828
7992
  import net from "net";
7829
- var logger2 = createServiceLogger("PtyStreamServer");
7993
+ var logger3 = createServiceLogger("PtyStreamServer");
7830
7994
  var RING_MAX_CHARS = 256 * 1024;
7831
7995
  var PtyStreamServer = class {
7832
7996
  constructor(options) {
7833
7997
  this.options = options;
7834
7998
  this.server = net.createServer((socket) => this.handleConnection(socket));
7835
7999
  this.server.on("error", (err) => {
7836
- logger2.warn(`PTY stream server error: ${err.message}`);
8000
+ logger3.warn(`PTY stream server error: ${err.message}`);
7837
8001
  });
7838
8002
  }
7839
8003
  options;
@@ -7874,7 +8038,7 @@ var PtyStreamServer = class {
7874
8038
  return port;
7875
8039
  }
7876
8040
  }
7877
- logger2.warn(`PTY stream server could not bind any port in ${base}..${base + attempts - 1}`);
8041
+ logger3.warn(`PTY stream server could not bind any port in ${base}..${base + attempts - 1}`);
7878
8042
  return null;
7879
8043
  }
7880
8044
  tryListen(port) {
@@ -7995,7 +8159,7 @@ var PtyStreamServer = class {
7995
8159
  };
7996
8160
 
7997
8161
  // src/harness/pty/direct-stream.ts
7998
- var logger3 = createServiceLogger("PtyDirectStream");
8162
+ var logger4 = createServiceLogger("PtyDirectStream");
7999
8163
  var RELAY_COALESCE_MS = 2e3;
8000
8164
  var RELAY_MAX_BUFFER_CHARS = 48 * 1024;
8001
8165
  var DirectStreamController = class {
@@ -8044,7 +8208,7 @@ var DirectStreamController = class {
8044
8208
  });
8045
8209
  void created.listen().then((port) => this.onListening(created, port)).catch((err) => {
8046
8210
  this.starting = false;
8047
- logger3.warn(
8211
+ logger4.warn(
8048
8212
  `PTY stream server failed to start: ${err instanceof Error ? err.message : String(err)}`
8049
8213
  );
8050
8214
  });
@@ -8057,7 +8221,7 @@ var DirectStreamController = class {
8057
8221
  }
8058
8222
  this.server = created;
8059
8223
  this.reporter.reportPtyStream(port);
8060
- logger3.info(`PTY stream server listening on ${port} (session ${this.reporter.sessionId})`);
8224
+ logger4.info(`PTY stream server listening on ${port} (session ${this.reporter.sessionId})`);
8061
8225
  }
8062
8226
  /** Push the min box across both transports to the pty. */
8063
8227
  applyDims() {
@@ -10083,7 +10247,10 @@ var getTaskContract = defineToolContract({
10083
10247
  fields: {
10084
10248
  projectId: mcpProjectId,
10085
10249
  taskId: f.string({
10086
- desc: "The task ID or slug (the value in a card URL, /cards/<slug>)"
10250
+ // The path form is MCP-only on purpose: an in-pod agent is bound to one
10251
+ // project, where a bare slug already resolves, so the `agent` half of
10252
+ // this contract deliberately says nothing about it.
10253
+ desc: "The task ID or slug (the value in a card URL, /cards/<slug>), or a path-form reference '<project-slug>/<card-slug>' from the board's Copy path action"
10087
10254
  })
10088
10255
  }
10089
10256
  }
@@ -11064,6 +11231,80 @@ function buildGetSuggestionsTool(connection) {
11064
11231
 
11065
11232
  // src/tools/mutation-tools.ts
11066
11233
  import { z as z13 } from "zod";
11234
+
11235
+ // src/runner/refresh-verify-heal.ts
11236
+ async function refreshAndVerifyGithubCredential(cwd, mint) {
11237
+ const first = await mintWriteProbe(cwd, mint, false);
11238
+ if (first.probe.ok) return first;
11239
+ if (first.probe.outcome !== "denied") return first;
11240
+ if (!first.written) return first;
11241
+ const healed = await mintWriteProbe(cwd, mint, true);
11242
+ return { ...healed, healAttempted: true };
11243
+ }
11244
+ async function mintWriteProbe(cwd, mint, forceFresh) {
11245
+ let token;
11246
+ try {
11247
+ token = await mint(forceFresh ? { forceFresh: true } : {});
11248
+ } catch (err) {
11249
+ return {
11250
+ ok: false,
11251
+ probe: { ok: false, outcome: "unavailable", error: errorText(err) },
11252
+ healAttempted: false,
11253
+ failures: [`- could not mint a GitHub token: ${errorText(err)}`]
11254
+ };
11255
+ }
11256
+ if (!token) {
11257
+ return {
11258
+ ok: false,
11259
+ probe: { ok: false, outcome: "unavailable", error: "no token returned" },
11260
+ healAttempted: false,
11261
+ failures: ["- the server returned no GitHub token"]
11262
+ };
11263
+ }
11264
+ const written = await updateRemoteToken(cwd, token);
11265
+ process.env.GITHUB_TOKEN = token;
11266
+ process.env.GH_TOKEN = token;
11267
+ const probe = await verifyGitCredential(cwd);
11268
+ const failures = describeFailures(written, probe);
11269
+ return {
11270
+ ok: probe.ok && failures.length === 0,
11271
+ probe,
11272
+ written,
11273
+ healAttempted: false,
11274
+ failures,
11275
+ ...probe.ok ? {} : { tokenShape: describeTokenFile() }
11276
+ };
11277
+ }
11278
+ function describeFailures(written, probe) {
11279
+ const failures = [];
11280
+ const credentialError = written.credential.error ? `: ${written.credential.error}` : "";
11281
+ if (!written.credential.storeWritten) {
11282
+ failures.push(`- git credential store NOT written${credentialError}`);
11283
+ } else if (!written.credential.helperConfigured) {
11284
+ failures.push(`- git credential helper NOT configured${credentialError}`);
11285
+ }
11286
+ if (!written.files.tokenFile) {
11287
+ const why = written.files.errors?.tokenFile;
11288
+ failures.push(`- token file NOT written${why ? `: ${why}` : ""}`);
11289
+ }
11290
+ if (!written.files.ghHosts && !ghHostsExternallyOwned()) {
11291
+ const why = written.files.errors?.ghHosts;
11292
+ failures.push(`- gh CLI config NOT written${why ? `: ${why}` : ""}`);
11293
+ }
11294
+ if (probe.outcome === "denied") {
11295
+ failures.push(`- git could not authenticate to origin: ${probe.error ?? "unknown error"}`);
11296
+ } else if (probe.outcome === "timeout") {
11297
+ failures.push(
11298
+ `- the credential is UNPROVEN: the check against origin timed out (${probe.error ?? "killed"}). The pod is likely under load \u2014 this is not evidence the token is bad.`
11299
+ );
11300
+ }
11301
+ return failures;
11302
+ }
11303
+ function errorText(err) {
11304
+ return err instanceof Error ? err.message : String(err);
11305
+ }
11306
+
11307
+ // src/tools/mutation-tools.ts
11067
11308
  function buildPostToChatTool(connection) {
11068
11309
  return defineContractTool(
11069
11310
  postToChatContract,
@@ -11353,60 +11594,85 @@ function buildVoteSuggestionTool(connection) {
11353
11594
  }
11354
11595
  );
11355
11596
  }
11597
+ function describeRefreshOutcome(outcome, tokenFile) {
11598
+ const healedNote = outcome.healAttempted ? " (the first attempt was rejected; a force-fresh retry fixed it)" : "";
11599
+ if (outcome.probe.ok) {
11600
+ if (outcome.failures.length === 0) {
11601
+ return {
11602
+ report: false,
11603
+ text: [
11604
+ `GitHub token refreshed and verified against origin${healedNote}.`,
11605
+ "- git: the credential store and helper are updated, so `git push` works now with no extra step.",
11606
+ "- gh: the CLI config is updated, so `gh` works now with no extra step.",
11607
+ `- shell/scripts: read the current token from ${tokenFile}.`,
11608
+ ` For a command that needs it in the environment: GITHUB_TOKEN=$(cat ${tokenFile}) <command>`,
11609
+ "Retry the failed command."
11610
+ ].join("\n")
11611
+ };
11612
+ }
11613
+ return {
11614
+ report: false,
11615
+ text: [
11616
+ `GitHub token refreshed, but some credential copies did not update.`,
11617
+ ...outcome.failures,
11618
+ `A current token is available at ${tokenFile} when that file was written.`
11619
+ ].join("\n")
11620
+ };
11621
+ }
11622
+ if (outcome.probe.outcome === "timeout") {
11623
+ return {
11624
+ report: false,
11625
+ text: [
11626
+ "GitHub token refreshed, but the check against origin timed out \u2014 the credential is UNPROVEN, not known bad.",
11627
+ ...outcome.failures,
11628
+ "The pod is probably under load (a heavy gate). Retry your git command directly; if it succeeds, nothing is wrong."
11629
+ ].join("\n")
11630
+ };
11631
+ }
11632
+ if (outcome.probe.outcome === "unavailable") {
11633
+ return {
11634
+ report: false,
11635
+ text: [
11636
+ "The server did not hand back a GitHub token, so nothing was refreshed and nothing was tested.",
11637
+ ...outcome.failures,
11638
+ "The agent's connection to the API may be down, or this project may have no GitHub App installed. Wait for the connection to recover and try again."
11639
+ ].join("\n")
11640
+ };
11641
+ }
11642
+ return {
11643
+ report: true,
11644
+ text: [
11645
+ `GitHub token refresh did NOT produce a working git credential${outcome.healAttempted ? ", including after a force-fresh retry" : ""}.`,
11646
+ ...outcome.failures,
11647
+ ...outcome.tokenShape ? [`- credential in play: ${outcome.tokenShape}`] : [],
11648
+ "This has been reported to the card, so the team can see it.",
11649
+ "Do not retry this tool. **Your unpushed work is only on this pod** \u2014 the WIP snapshot needs the same credential, so it is not being saved either. Preserve it before anything else: `git bundle create /tmp/work.bundle --all` (or `git format-patch`), then attach that file to the card with `upload_attachment`."
11650
+ ].join("\n")
11651
+ };
11652
+ }
11356
11653
  function buildRefreshGithubTokenTool(connection, config) {
11357
11654
  return defineTool(
11358
11655
  "refresh_github_token",
11359
- "Mint a fresh GitHub token for this pod when git or gh fails with a 401 / 'Bad credentials'. The pod's token expires about every hour. This updates the git credential store and the gh CLI config, and returns the shell command that puts a current token in your environment.",
11656
+ "Mint a fresh GitHub token for this pod when git or gh fails with a 401 / 'Bad credentials'. The pod's token expires about every hour. This updates the git credential store and the gh CLI config, verifies the result against origin, and returns the shell command that puts a current token in your environment.",
11360
11657
  {},
11361
11658
  async () => {
11362
11659
  try {
11363
- const result = await connection.call("refreshGithubToken", {
11364
- sessionId: connection.sessionId
11365
- });
11366
- const written = await updateRemoteToken(config.workspaceDir, result.token);
11367
- process.env.GITHUB_TOKEN = result.token;
11368
- process.env.GH_TOKEN = result.token;
11369
- const tokenFile = githubTokenFilePath();
11370
- const failures = [];
11371
- if (!written.credential.storeWritten) {
11372
- failures.push(
11373
- `- git credential store NOT written${written.credential.error ? `: ${written.credential.error}` : ""}`
11374
- );
11375
- } else if (!written.credential.helperConfigured) {
11376
- failures.push(
11377
- `- git credential helper NOT configured${written.credential.error ? `: ${written.credential.error}` : ""}`
11378
- );
11379
- }
11380
- if (!written.files.tokenFile) failures.push(`- token file ${tokenFile} NOT written`);
11381
- if (!written.files.ghHosts && !ghHostsExternallyOwned()) {
11382
- failures.push("- gh CLI config NOT written");
11383
- }
11384
- const probe = await verifyGitCredential(config.workspaceDir);
11385
- if (!probe.ok) {
11386
- failures.push(
11387
- `- git could not authenticate to origin: ${probe.error ?? "unknown error"}`
11388
- );
11389
- }
11390
- if (failures.length > 0) {
11391
- return textResult(
11392
- [
11393
- probe.ok ? "GitHub token refreshed, but some credential copies did not update." : "GitHub token refresh did NOT produce a working git credential.",
11394
- ...failures,
11395
- `A current token is available at ${tokenFile} when that file was written.`,
11396
- "Do not retry this tool more than once. Report the failing lines above to the team \u2014 the GitHub App install may be missing for this project, or the pod's credential directory may not be writable."
11397
- ].join("\n")
11398
- );
11399
- }
11400
- return textResult(
11401
- [
11402
- "GitHub token refreshed and verified against origin.",
11403
- "- git: the credential store and helper are updated, so `git push` works now with no extra step.",
11404
- "- gh: the CLI config is updated, so `gh` works now with no extra step.",
11405
- `- shell/scripts: read the current token from ${tokenFile}.`,
11406
- ` For a command that needs it in the environment: GITHUB_TOKEN=$(cat ${tokenFile}) <command>`,
11407
- "Retry the failed command."
11408
- ].join("\n")
11660
+ const outcome = await refreshAndVerifyGithubCredential(
11661
+ config.workspaceDir,
11662
+ (opts) => connection.call("refreshGithubToken", {
11663
+ sessionId: connection.sessionId,
11664
+ ...opts?.forceFresh ? { forceFresh: true } : {}
11665
+ }).then((r) => r.token)
11409
11666
  );
11667
+ const { text, report } = describeRefreshOutcome(outcome, githubTokenFilePath());
11668
+ if (report) {
11669
+ connection.reportCredentialFailure({
11670
+ error: outcome.failures.join("\n"),
11671
+ ...outcome.tokenShape ? { tokenShape: outcome.tokenShape } : {},
11672
+ healed: outcome.healAttempted
11673
+ });
11674
+ }
11675
+ return textResult(text);
11410
11676
  } catch (error) {
11411
11677
  return textResult(
11412
11678
  `Failed to refresh the GitHub token: ${error instanceof Error ? error.message : "Unknown error"}
@@ -12893,7 +13159,7 @@ function resolvePlaywrightMcpServer(env = process.env) {
12893
13159
  }
12894
13160
 
12895
13161
  // src/execution/event-handlers.ts
12896
- var logger4 = createServiceLogger("event-handlers");
13162
+ var logger5 = createServiceLogger("event-handlers");
12897
13163
  function safeVoid(promise, context) {
12898
13164
  if (promise && typeof promise.catch === "function") {
12899
13165
  promise.catch((err) => {
@@ -13048,7 +13314,7 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
13048
13314
  }
13049
13315
  function handleRateLimitEvent(event, host) {
13050
13316
  const { rate_limit_info } = event;
13051
- logger4.info("Rate limit event received", { rate_limit_info });
13317
+ logger5.info("Rate limit event received", { rate_limit_info });
13052
13318
  const status = rate_limit_info.status;
13053
13319
  const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
13054
13320
  if (utilization !== void 0 && rate_limit_info.rateLimitType) {
@@ -13772,7 +14038,7 @@ function buildCanUseTool(host) {
13772
14038
  }
13773
14039
 
13774
14040
  // src/execution/query-executor.ts
13775
- var logger5 = createServiceLogger("QueryExecutor");
14041
+ var logger6 = createServiceLogger("QueryExecutor");
13776
14042
  var IMAGE_ERROR_PATTERN2 = /Could not process image/i;
13777
14043
  var RETRY_DELAYS_MS2 = [6e4, 12e4, 18e4, 3e5];
13778
14044
  function buildHooks(host) {
@@ -13864,7 +14130,7 @@ function repairTornSessionFile(path2) {
13864
14130
  }
13865
14131
  if (keepEnd === content.length) return false;
13866
14132
  truncateSync(path2, Buffer.byteLength(content.slice(0, keepEnd), "utf8"));
13867
- logger5.warn("Repaired torn transcript before resume", {
14133
+ logger6.warn("Repaired torn transcript before resume", {
13868
14134
  path: path2,
13869
14135
  trimmedBytes: content.length - keepEnd
13870
14136
  });
@@ -13945,7 +14211,7 @@ function buildQueryOptions(host, context) {
13945
14211
  disallowedTools: buildDisallowedTools(settings, mode, host.hasExitedPlanMode),
13946
14212
  enableFileCheckpointing: settings.enableFileCheckpointing,
13947
14213
  stderr: (data) => {
13948
- logger5.warn("Claude Code stderr", { data: data.trimEnd() });
14214
+ logger6.warn("Claude Code stderr", { data: data.trimEnd() });
13949
14215
  }
13950
14216
  };
13951
14217
  }
@@ -14501,7 +14767,7 @@ async function runWithRetry(initialQuery, context, host, options) {
14501
14767
  }
14502
14768
 
14503
14769
  // src/runner/query-bridge.ts
14504
- var logger6 = createServiceLogger("QueryBridge");
14770
+ var logger7 = createServiceLogger("QueryBridge");
14505
14771
  function resolveHarnessKind() {
14506
14772
  if (process.env.CONVEYOR_FORCE_SDK_CARDS === "1") return "sdk";
14507
14773
  return "pty";
@@ -14683,9 +14949,9 @@ var QueryBridge = class {
14683
14949
  const msg = err instanceof Error ? err.message : String(err);
14684
14950
  const isAbort = this._stopped || /abort/i.test(msg);
14685
14951
  if (isAbort) {
14686
- logger6.info("Query stopped by user", { error: msg });
14952
+ logger7.info("Query stopped by user", { error: msg });
14687
14953
  } else {
14688
- logger6.error("Query execution failed", { error: msg });
14954
+ logger7.error("Query execution failed", { error: msg });
14689
14955
  this.connection.sendEvent({ type: "error", message: msg });
14690
14956
  }
14691
14957
  } finally {
@@ -14711,9 +14977,9 @@ var QueryBridge = class {
14711
14977
  const msg = err instanceof Error ? err.message : String(err);
14712
14978
  const isAbort = this._stopped || /abort/i.test(msg);
14713
14979
  if (isAbort) {
14714
- logger6.info("Passive turn stopped", { error: msg });
14980
+ logger7.info("Passive turn stopped", { error: msg });
14715
14981
  } else {
14716
- logger6.error("Passive turn failed", { error: msg });
14982
+ logger7.error("Passive turn failed", { error: msg });
14717
14983
  this.connection.sendEvent({ type: "error", message: msg });
14718
14984
  }
14719
14985
  } finally {
@@ -15040,7 +15306,8 @@ async function runUsageProbe(deps = {}) {
15040
15306
  }
15041
15307
 
15042
15308
  // src/execution/usage-sampler.ts
15043
- var logger7 = createServiceLogger("usage-sampler");
15309
+ var logger8 = createServiceLogger("usage-sampler");
15310
+ var NO_SAMPLES = { samples: [], unmeasurable: null };
15044
15311
  function isAttributable(identity, sessionToken) {
15045
15312
  if (!identity) return { ok: true };
15046
15313
  if (identity.hasRefreshToken) {
@@ -15052,14 +15319,14 @@ function isAttributable(identity, sessionToken) {
15052
15319
  return { ok: true };
15053
15320
  }
15054
15321
  async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscriptionCredentials = () => existsSync3(claudeCredentialsPath()), readIdentity = readCredentialsIdentity) {
15055
- if (!token && !hasSubscriptionCredentials()) return [];
15322
+ if (!token && !hasSubscriptionCredentials()) return NO_SAMPLES;
15056
15323
  try {
15057
15324
  const attributable = isAttributable(await readIdentity(), token);
15058
15325
  if (!attributable.ok) {
15059
- logger7.info("usage sample skipped \u2014 credentials not attributable to this session's key", {
15326
+ logger8.info("usage sample skipped \u2014 credentials not attributable to this session's key", {
15060
15327
  reason: attributable.reason
15061
15328
  });
15062
- return [];
15329
+ return { samples: [], unmeasurable: { reason: attributable.reason ?? "unattributable" } };
15063
15330
  }
15064
15331
  const stdout = await probe();
15065
15332
  const { sessionUsage, weeklyUsage, sessionResetsAt, weeklyResetsAt, gauges } = parseUsageGauges(stdout);
@@ -15083,18 +15350,37 @@ async function sampleKeyUsage(token, probe = () => runUsageProbe(), hasSubscript
15083
15350
  });
15084
15351
  }
15085
15352
  if (samples.length === 0) {
15086
- logger7.info("usage sample produced no gauges", {
15353
+ logger8.info("usage sample produced no gauges", {
15087
15354
  stdoutLength: stdout.length,
15088
15355
  stdoutHead: stdout.slice(0, 200).replaceAll("\n", " ")
15089
15356
  });
15090
15357
  }
15091
- return samples;
15358
+ return { samples, unmeasurable: null };
15092
15359
  } catch (error) {
15093
- logger7.info("usage sample failed", {
15360
+ logger8.info("usage sample failed", {
15094
15361
  error: error instanceof Error ? error.message : String(error)
15095
15362
  });
15096
- return [];
15097
- }
15363
+ return NO_SAMPLES;
15364
+ }
15365
+ }
15366
+ function buildRateLimitEvents(samples, codingAgentKeyId) {
15367
+ return samples.map((sample) => ({
15368
+ type: "rate_limit_update",
15369
+ rateLimitType: sample.rateLimitType,
15370
+ utilization: sample.utilization,
15371
+ status: sample.status,
15372
+ resetsAt: sample.resetsAt ?? void 0,
15373
+ gauges: sample.gauges,
15374
+ ...codingAgentKeyId ? { codingAgentKeyId } : {}
15375
+ }));
15376
+ }
15377
+ function buildUnmeasurableEvent(reason, codingAgentKeyId) {
15378
+ return {
15379
+ type: "rate_limit_update",
15380
+ unmeasurable: true,
15381
+ reason,
15382
+ ...codingAgentKeyId ? { codingAgentKeyId } : {}
15383
+ };
15098
15384
  }
15099
15385
 
15100
15386
  // src/setup/git-ready.ts
@@ -16243,12 +16529,9 @@ var SessionRunner = class _SessionRunner {
16243
16529
  /** Shared token-refresh closure for the git-flush call sites (periodic
16244
16530
  * backstop + per-turn flush). Never throws — returns undefined so the
16245
16531
  * caller falls back to its existing remote credentials. */
16246
- async refreshGithubTokenForFlush() {
16532
+ async refreshGithubTokenForFlush(opts) {
16247
16533
  try {
16248
- const res = await this.connection.call("refreshGithubToken", {
16249
- sessionId: this.connection.sessionId
16250
- });
16251
- return res.token;
16534
+ return await this.mintGithubToken(opts);
16252
16535
  } catch {
16253
16536
  return void 0;
16254
16537
  }
@@ -16263,7 +16546,7 @@ var SessionRunner = class _SessionRunner {
16263
16546
  try {
16264
16547
  await flushPendingChanges(this.config.workspaceDir, {
16265
16548
  wipMessage,
16266
- refreshToken: () => this.refreshGithubTokenForFlush()
16549
+ refreshToken: (opts) => this.refreshGithubTokenForFlush(opts)
16267
16550
  });
16268
16551
  } catch {
16269
16552
  } finally {
@@ -16288,7 +16571,7 @@ var SessionRunner = class _SessionRunner {
16288
16571
  try {
16289
16572
  const result = await flushAllPendingWork(this.config.workspaceDir, {
16290
16573
  wipMessage: "WIP: periodic auto-commit",
16291
- refreshToken: () => this.refreshGithubTokenForFlush()
16574
+ refreshToken: (opts) => this.refreshGithubTokenForFlush(opts)
16292
16575
  });
16293
16576
  if (result.hadWork) {
16294
16577
  process.stderr.write(
@@ -16303,22 +16586,20 @@ var SessionRunner = class _SessionRunner {
16303
16586
  }
16304
16587
  /** Sample the running Claude subscription key's rate-limit utilization from
16305
16588
  * the Claude CLI `/usage` command and report it as `rate_limit_update` events.
16306
- * The API (`persistRateLimitSnapshot`) attributes them to the key this session
16307
- * launched under, keeping User Settings + the PtY-tab usage widget fresh and
16589
+ * The API (`persistRateLimitSnapshot`) attributes them to the key named on the
16590
+ * event, keeping User Settings + the PtY-tab usage widget fresh and
16308
16591
  * `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
16309
16592
  * pod has no OAuth token (e.g. API-key projects). */
16310
16593
  async sampleAndReportKeyUsage() {
16311
16594
  if (this.stopped) return;
16312
- const samples = await sampleKeyUsage(process.env.CLAUDE_CODE_OAUTH_TOKEN);
16313
- for (const sample of samples) {
16314
- this.connection.sendEvent({
16315
- type: "rate_limit_update",
16316
- rateLimitType: sample.rateLimitType,
16317
- utilization: sample.utilization,
16318
- status: sample.status,
16319
- resetsAt: sample.resetsAt ?? void 0,
16320
- gauges: sample.gauges
16321
- });
16595
+ const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
16596
+ const { samples, unmeasurable } = await sampleKeyUsage(process.env.CLAUDE_CODE_OAUTH_TOKEN);
16597
+ if (unmeasurable) {
16598
+ this.connection.sendEvent(buildUnmeasurableEvent(unmeasurable.reason, codingAgentKeyId));
16599
+ return;
16600
+ }
16601
+ for (const event of buildRateLimitEvents(samples, codingAgentKeyId)) {
16602
+ this.connection.sendEvent(event);
16322
16603
  }
16323
16604
  }
16324
16605
  /** Wait (bounded) for any in-flight periodic/turn flush to release the shared
@@ -16347,16 +16628,7 @@ var SessionRunner = class _SessionRunner {
16347
16628
  try {
16348
16629
  const result = await flushAllPendingWork(this.config.workspaceDir, {
16349
16630
  wipMessage: "WIP: auto-commit on conveyor-agent shutdown",
16350
- refreshToken: async () => {
16351
- try {
16352
- const res = await this.connection.call("refreshGithubToken", {
16353
- sessionId: this.connection.sessionId
16354
- });
16355
- return res.token;
16356
- } catch {
16357
- return void 0;
16358
- }
16359
- }
16631
+ refreshToken: (opts) => this.refreshGithubTokenForFlush(opts)
16360
16632
  });
16361
16633
  if (result.hadWork) {
16362
16634
  process.stderr.write(
@@ -16537,22 +16809,42 @@ var SessionRunner = class _SessionRunner {
16537
16809
  void handlePullBranch(this.config.workspaceDir, branch);
16538
16810
  });
16539
16811
  }
16540
- /** Proactively refresh the GitHub token before the 1-hour expiry. */
16812
+ /**
16813
+ * Proactively refresh the GitHub token before the 1-hour expiry — and PROVE
16814
+ * the result works.
16815
+ *
16816
+ * The old version logged "Proactively refreshed GitHub token" whenever the
16817
+ * credential store write succeeded. It never inspected the token-file write
16818
+ * and never asked git anything, so a pod whose token file was stale printed
16819
+ * that line every 45 minutes while every push 401'd. Three of those lines,
16820
+ * perfectly spaced, are what made the 2026-08-19 occurrence unreadable. A
16821
+ * success line here now means the credential authenticated against origin.
16822
+ */
16541
16823
  async refreshGithubToken() {
16542
16824
  try {
16543
- const res = await this.connection.call("refreshGithubToken", {
16544
- sessionId: this.connection.sessionId
16545
- });
16546
- const written = await updateRemoteToken(this.config.workspaceDir, res.token);
16547
- process.env.GITHUB_TOKEN = res.token;
16548
- process.env.GH_TOKEN = res.token;
16549
- if (written.credential.ok) {
16550
- process.stderr.write("[conveyor-agent] Proactively refreshed GitHub token\n");
16551
- } else {
16825
+ const outcome = await refreshAndVerifyGithubCredential(
16826
+ this.config.workspaceDir,
16827
+ (opts) => this.mintGithubToken(opts)
16828
+ );
16829
+ if (outcome.ok) {
16552
16830
  process.stderr.write(
16553
- `[conveyor-agent] Warning: refreshed GitHub token but the git credential did not update: ${written.credential.error ?? "unknown error"}
16831
+ `[conveyor-agent] Proactively refreshed GitHub token \u2014 verified against origin${outcome.healAttempted ? " (after a force-fresh retry)" : ""}
16554
16832
  `
16555
16833
  );
16834
+ return;
16835
+ }
16836
+ process.stderr.write(
16837
+ `[conveyor-agent] Warning: proactive GitHub token refresh did not produce a working credential:
16838
+ ${outcome.failures.join("\n")}
16839
+ ` + (outcome.tokenShape ? ` token file: ${outcome.tokenShape}
16840
+ ` : "")
16841
+ );
16842
+ if (outcome.probe.outcome === "denied") {
16843
+ this.connection.reportCredentialFailure({
16844
+ error: outcome.failures.join("\n"),
16845
+ ...outcome.tokenShape ? { tokenShape: outcome.tokenShape } : {},
16846
+ healed: outcome.healAttempted
16847
+ });
16556
16848
  }
16557
16849
  } catch (err) {
16558
16850
  const msg = err instanceof Error ? err.message : String(err);
@@ -16560,6 +16852,14 @@ var SessionRunner = class _SessionRunner {
16560
16852
  `);
16561
16853
  }
16562
16854
  }
16855
+ /** Mint a GitHub token over the RPC. `forceFresh` bypasses the API's cache. */
16856
+ async mintGithubToken(opts) {
16857
+ const res = await this.connection.call("refreshGithubToken", {
16858
+ sessionId: this.connection.sessionId,
16859
+ ...opts?.forceFresh ? { forceFresh: true } : {}
16860
+ });
16861
+ return res.token;
16862
+ }
16563
16863
  /** Re-fetch task context to pick up a newly created branch and check it out. */
16564
16864
  async refreshBranchForBuilding() {
16565
16865
  try {
@@ -16732,6 +17032,7 @@ function unshallowRepo(workspaceDir) {
16732
17032
  export {
16733
17033
  fetchBootstrap,
16734
17034
  applyBootstrapToEnv,
17035
+ createServiceLogger,
16735
17036
  AgentConnection,
16736
17037
  DEFAULT_SONNET_MODEL,
16737
17038
  isPermissionDeniedError,
@@ -16739,7 +17040,6 @@ export {
16739
17040
  Lifecycle,
16740
17041
  buildSynthesizedCredentials,
16741
17042
  claudeJsonPath,
16742
- createServiceLogger,
16743
17043
  PtyHarness,
16744
17044
  resolveTuiKindFromEnv,
16745
17045
  resolveTuiAdapter,
@@ -16747,11 +17047,11 @@ export {
16747
17047
  statWorkspacePath,
16748
17048
  workspacePathExists,
16749
17049
  GIT_TIMEOUT_MS,
17050
+ updateRemoteToken,
16750
17051
  hasUncommittedChanges,
16751
17052
  getCurrentBranch,
16752
17053
  hasUnpushedCommits,
16753
17054
  stageAndCommit,
16754
- updateRemoteToken,
16755
17055
  flushPendingChanges,
16756
17056
  pushToOrigin,
16757
17057
  buildProjectTools,
@@ -16760,6 +17060,8 @@ export {
16760
17060
  parseUsageGauges,
16761
17061
  runUsageProbe,
16762
17062
  sampleKeyUsage,
17063
+ buildRateLimitEvents,
17064
+ buildUnmeasurableEvent,
16763
17065
  awaitGitReady,
16764
17066
  PortDiscovery,
16765
17067
  CodespacePortVisibility,
@@ -16769,4 +17071,3 @@ export {
16769
17071
  loadConveyorConfig,
16770
17072
  unshallowRepo
16771
17073
  };
16772
- //# sourceMappingURL=chunk-BAQ2OAKB.js.map