@miraland-labs/conduit-bridge 0.16.32 → 0.16.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/driver.js CHANGED
@@ -3,9 +3,9 @@ import { boundedTail } from "./ensure-test-evidence.js";
3
3
  import { resolveAgentTimeout } from "./execution-budget.js";
4
4
  import { existsSync } from "node:fs";
5
5
  import { mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
6
- import { join } from "node:path";
6
+ import { join, resolve } from "node:path";
7
7
  import { z } from "zod";
8
- import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectGrok, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
8
+ import { allowsExternalFetch, deniedCommands, executionClassPromptRules, isRunnableVerificationCommand, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectGrok, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
9
9
  /**
10
10
  * The bound for this turn.
11
11
  *
@@ -995,10 +995,16 @@ export const kiroDriver = {
995
995
  },
996
996
  };
997
997
  /**
998
- * Antigravity's `agy` CLI (verified against agy 1.1.3): non-interactive runs use
999
- * `-p`/`--print` with `--mode plan` (read-only) or `--mode accept-edits`. Its
1000
- * enforcement is mode-level, not per-tool coarser than Claude, the Cursor tier.
1001
- * Write mode adds `--sandbox` for terminal restrictions. Output is plain text.
998
+ * Antigravity's `agy` CLI (verified against agy 1.1.24): non-interactive runs use
999
+ * `--mode plan` (read-only) or `--mode accept-edits`. `-p`/`--print` is a Go-style
1000
+ * flag that consumes its value from the next argument, so the prompt must be the
1001
+ * last argument attached to `-p` putting other flags after it (as earlier builds
1002
+ * of this driver did) makes agy swallow one of them as the prompt and silently
1003
+ * drop the real prompt. `--print-timeout` is set from the turn budget so agy's
1004
+ * 5-minute default cannot cut a longer turn short. Its enforcement is mode-level,
1005
+ * not per-tool — coarser than Claude, the Cursor tier. Write mode adds `--sandbox`
1006
+ * for terminal restrictions. `--output-format json` returns one envelope, parsed by
1007
+ * parseAntigravityOutput.
1002
1008
  */
1003
1009
  export function antigravityModeForGrants(grants, executionClass) {
1004
1010
  if (!hasMappedRepoAccess(grants))
@@ -1011,15 +1017,206 @@ export function antigravityModeForGrants(grants, executionClass) {
1011
1017
  return "plan";
1012
1018
  return null;
1013
1019
  }
1014
- export function antigravityRunArgs(input, mode) {
1015
- const args = ["-p", "--mode", mode];
1020
+ /** agy's `--print-timeout` takes a Go duration; ceil to whole minutes (agy's own default floor is 5m). */
1021
+ export function antigravityPrintTimeout(timeoutMs) {
1022
+ return `${Math.max(5, Math.ceil(timeoutMs / 60_000))}m0s`;
1023
+ }
1024
+ /**
1025
+ * Class + grants → agy permission allow-rules (probed against agy 1.1.24).
1026
+ *
1027
+ * Headless `agy -p` soft-denies every tool it was not granted: the run returns an empty response
1028
+ * with a stderr notice and nothing is written. Only wildcard rules ever matched in the probes —
1029
+ * bounded ones (`command(git add)`, `command(git *)`, and in the field `unsandboxed(git push)`)
1030
+ * were auto-denied, because the agent picks its own shell spellings and stops at the first denial.
1031
+ * So the enforceable unit is the coarse rule plus `--sandbox`, the same boundary the Codex driver
1032
+ * leans on with `--sandbox workspace-write`. The grammar agy accepts is `command(<cmd>)`,
1033
+ * `unsandboxed(<cmd>)`, `write_file(<glob>)`, `read_file(<glob>)`, `read_url(<target>)`.
1034
+ *
1035
+ * `deniedCommands` cannot be expressed here: agy has no deny grammar. Antigravity is the coarse
1036
+ * tier the product spec already assigns it — enforcement is the sandbox, the prompt rule that names
1037
+ * the hard-denied commands (executionClassPromptRules), and the delivery gate.
1038
+ */
1039
+ export function antigravityPermissionProjection(executionClass, grants, options = {}) {
1040
+ const allow = ["read_file(*)"];
1041
+ // Live HTTP is the assignment's capability, read the one way every driver reads it. agy aborts the
1042
+ // whole turn on a denial rather than skipping the tool, so an assignment that may fetch must carry
1043
+ // the rule; one that may not never gets it.
1044
+ if (allowsExternalFetch(executionClass, options.capabilities))
1045
+ allow.push("read_url(*)");
1046
+ const runnable = (options.verificationCommands ?? []).some(isRunnableVerificationCommand);
1047
+ if (executionClass === "observe" || executionClass === "observe_network" || options.diagnosis) {
1048
+ // Diagnosis has to run its bounded checks; a plain observe class gets no shell at all.
1049
+ if (runnable && (options.diagnosis || grants.includes("test_run")))
1050
+ allow.push("command(*)");
1051
+ return allow;
1052
+ }
1053
+ if (executionClass === "verify") {
1054
+ allow.push("command(*)");
1055
+ return allow;
1056
+ }
1057
+ allow.push("write_file(*)", "command(*)");
1058
+ // Land work needs the sandbox escape hatch, and only a wildcard reaches it: a real mutate_repo
1059
+ // assignment carrying `pr_create` was auto-denied on exact `unsandboxed(git push)` with
1060
+ // "a tool required the \"unsandboxed\" permission that headless mode cannot prompt for". So for
1061
+ // the land class the sandbox boundary no longer covers the escape hatch, and enforcement there is
1062
+ // the prompt's hard-deny list, the delivery gate (one PR, a scoped diff, Bridge-witnessed
1063
+ // commands) and the project's grants — the coarse tier. Without `pr_create` no `unsandboxed(...)`
1064
+ // rule is emitted at all, and `--sandbox` stays on either way.
1065
+ if (grants.includes("pr_create"))
1066
+ allow.push("unsandboxed(*)");
1067
+ return allow;
1068
+ }
1069
+ /**
1070
+ * The one driver note agy needs beside the class rules. It aborts the whole turn on any denied tool
1071
+ * — Claude Code merely skips one — and a run that had already committed died opening its own PR URL
1072
+ * with `read_url`, which no assignment without external_network may hold.
1073
+ */
1074
+ export const antigravityPromptRule = "Antigravity: do not use read_url, browser, or web tools unless this assignment lists external_network; do not open the pull request URL after creating it — report the URL from the `gh pr create` output. A denied tool ends the whole run, so use only the tools this assignment allows.";
1075
+ export function antigravityRunArgs(input, mode, timeoutMs) {
1076
+ const args = ["--mode", mode];
1016
1077
  if (mode === "accept-edits")
1017
1078
  args.push("--sandbox");
1018
- args.push(input.prompt);
1079
+ args.push("--output-format", "json");
1080
+ args.push("--print-timeout", antigravityPrintTimeout(timeoutMs));
1081
+ // agy's CLI default model is one account lane among several; `--model` selects the lane this
1082
+ // machine's tier mapping names, which is how a spent Gemini window stops the whole driver.
1083
+ if (input.model)
1084
+ args.push("--model", input.model);
1085
+ if (input.resumeSessionId)
1086
+ args.push("--conversation", input.resumeSessionId);
1087
+ args.push("-p", input.prompt);
1019
1088
  return args;
1020
1089
  }
1090
+ /**
1091
+ * Parse `agy models`: one model per line, the id first and the display name after it
1092
+ * ("claude-sonnet-4-6 Claude Sonnet 4.6 (Thinking)"). Two or more spaces separate the two
1093
+ * columns, which is what tells a model line from a banner line ("Available models:").
1094
+ */
1095
+ export function parseAntigravityModelList(stdout) {
1096
+ const models = new Set();
1097
+ for (const line of stdout.split("\n")) {
1098
+ const match = /^\s*([A-Za-z0-9][A-Za-z0-9._/-]*)\s{2,}\S/.exec(line);
1099
+ if (match)
1100
+ models.add(match[1]);
1101
+ }
1102
+ return models;
1103
+ }
1104
+ /**
1105
+ * `agy --output-format json` prints one envelope:
1106
+ * `{"conversation_id","status","response","duration_seconds","num_turns","usage"}`.
1107
+ * Tolerant like the Cursor parser: a build that answers in plain text still delivers its report.
1108
+ *
1109
+ * A refused run keeps the same envelope with `status: "ERROR"`, an empty `response`, and the reason
1110
+ * in `error` ("Individual quota reached ... Resets in 165h48m43s."), and prints **nothing** on
1111
+ * stderr. Reading only `response` reported that run as "agy exited with code 1", which says nothing
1112
+ * the owner can act on — so `status` and `error` are parsed here and named by the driver.
1113
+ */
1114
+ export function parseAntigravityOutput(stdout) {
1115
+ try {
1116
+ const parsed = JSON.parse(stdout.trim());
1117
+ return {
1118
+ resultText: typeof parsed.response === "string" ? parsed.response : null,
1119
+ sessionId: typeof parsed.conversation_id === "string" ? parsed.conversation_id : null,
1120
+ status: typeof parsed.status === "string" ? parsed.status : null,
1121
+ error: typeof parsed.error === "string" && parsed.error.trim() ? parsed.error : null,
1122
+ };
1123
+ }
1124
+ catch {
1125
+ return { resultText: stdout || null, sessionId: null, status: null, error: null };
1126
+ }
1127
+ }
1128
+ /**
1129
+ * The stderr line agy prints when a permission it was not granted ended the turn. An empty response
1130
+ * beside such a line is a denial, not an agent with nothing to say, and the control plane has to see
1131
+ * the difference.
1132
+ */
1133
+ export function antigravityDenialNotice(stderr) {
1134
+ for (const line of stderr.split("\n")) {
1135
+ const trimmed = line.trim();
1136
+ if (trimmed && /no output produced|permission/i.test(trimmed))
1137
+ return trimmed;
1138
+ }
1139
+ return null;
1140
+ }
1141
+ /** agy reads one global settings file, so concurrent runs are serialized rather than racing on it. */
1142
+ let antigravitySettingsLock = Promise.resolve();
1143
+ /**
1144
+ * Write the run's permission contract to agy's global settings file, restoring it byte-exactly after.
1145
+ *
1146
+ * Probed against agy 1.1.24: this is the only placement a headless run honours — a workspace-local
1147
+ * `.antigravity/settings.json` and `~/.gemini/config/projects/<uuid>.json` `permissionGrants` were
1148
+ * both ignored, and without `trustedWorkspaces` naming the workspace every tool is soft-denied. The
1149
+ * file belongs to the machine owner and is global, so the original bytes are held in memory and
1150
+ * beside it as `settings.json.conduit-backup` for the length of the run, unparsable JSON fails the
1151
+ * run instead of being overwritten, and every other key is preserved.
1152
+ */
1153
+ async function withAntigravityPermissions(workspace, allow, run) {
1154
+ const previousRun = antigravitySettingsLock;
1155
+ let release = () => undefined;
1156
+ antigravitySettingsLock = new Promise((settle) => { release = settle; });
1157
+ await previousRun.catch(() => undefined);
1158
+ try {
1159
+ const home = process.env.HOME;
1160
+ if (!home)
1161
+ throw new Error("antigravity needs HOME to write ~/.gemini/antigravity-cli/settings.json");
1162
+ const settingsDir = join(home, ".gemini", "antigravity-cli");
1163
+ const settingsPath = join(settingsDir, "settings.json");
1164
+ const backupPath = `${settingsPath}.conduit-backup`;
1165
+ const settingsDirExisted = existsSync(settingsDir);
1166
+ let previous = null;
1167
+ try {
1168
+ previous = await readFile(settingsPath, "utf8");
1169
+ }
1170
+ catch {
1171
+ previous = null;
1172
+ }
1173
+ let settings = {};
1174
+ if (previous !== null) {
1175
+ try {
1176
+ settings = { ...JSON.parse(previous) };
1177
+ }
1178
+ catch {
1179
+ throw new Error(`antigravity settings at ${settingsPath} are not valid JSON; refusing to overwrite them`);
1180
+ }
1181
+ await writeFile(backupPath, previous, "utf8");
1182
+ }
1183
+ const existingPermissions = (settings.permissions && typeof settings.permissions === "object" && !Array.isArray(settings.permissions))
1184
+ ? { ...settings.permissions }
1185
+ : {};
1186
+ const trusted = Array.isArray(settings.trustedWorkspaces)
1187
+ ? settings.trustedWorkspaces.filter((entry) => typeof entry === "string")
1188
+ : [];
1189
+ const absolute = resolve(workspace);
1190
+ settings.permissions = { ...existingPermissions, allow };
1191
+ settings.trustedWorkspaces = trusted.includes(absolute) ? trusted : [...trusted, absolute];
1192
+ await mkdir(settingsDir, { recursive: true });
1193
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
1194
+ try {
1195
+ return await run();
1196
+ }
1197
+ finally {
1198
+ if (previous === null)
1199
+ await rm(settingsPath, { force: true });
1200
+ else
1201
+ await writeFile(settingsPath, previous, "utf8");
1202
+ await rm(backupPath, { force: true });
1203
+ if (!settingsDirExisted)
1204
+ await rmdir(settingsDir).catch(() => undefined);
1205
+ }
1206
+ }
1207
+ finally {
1208
+ release();
1209
+ }
1210
+ }
1021
1211
  export const antigravityDriver = {
1022
1212
  name: "antigravity",
1213
+ async listModels(executable = "agy", workspace = process.cwd()) {
1214
+ const result = await execute(executable, ["models"], workspace, 30_000, undefined, "local");
1215
+ if (result.code !== 0)
1216
+ return null;
1217
+ const models = parseAntigravityModelList(result.stdout);
1218
+ return models.size ? models : null;
1219
+ },
1023
1220
  async run(input) {
1024
1221
  const fuelSource = input.fuelSource === "local" ? "local" : "conduit";
1025
1222
  // Antigravity authenticates with a Google login / GEMINI_API_KEY — not Conduit /v1 shims.
@@ -1034,12 +1231,33 @@ export const antigravityDriver = {
1034
1231
  if (!mode) {
1035
1232
  return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped Antigravity mode for active grants; refusing to start agent" };
1036
1233
  }
1037
- // agy print mode emits plain text and does not surface a resumable id, so rework resume is not wired.
1038
- const { code, stdout, stderr } = await execute(input.executable ?? "agy", antigravityRunArgs({ prompt: input.prompt, grants: input.grants }, mode), input.workspace, agentTurnTimeoutMs(input), undefined, "local");
1039
- if (code !== 0) {
1040
- return { status: "failed", resultText: stdout || null, sessionId: null, error: boundedTail(stderr || stdout || `agy exited with code ${code}`, 20_000) };
1234
+ const allow = antigravityPermissionProjection(executionClass, input.grants, {
1235
+ verificationCommands: input.verificationCommands,
1236
+ capabilities: input.capabilities ?? [],
1237
+ diagnosis: input.workRole === "diagnose",
1238
+ });
1239
+ const timeoutMs = agentTurnTimeoutMs(input);
1240
+ let executed;
1241
+ try {
1242
+ executed = await withAntigravityPermissions(input.workspace, allow, () => execute(input.executable ?? "agy", antigravityRunArgs({ prompt: `${input.prompt}\n\n${antigravityPromptRule}`, grants: input.grants, resumeSessionId: input.resumeSessionId, model: input.model }, mode, timeoutMs), input.workspace, timeoutMs, undefined, "local"));
1041
1243
  }
1042
- return { status: "completed", resultText: stdout, sessionId: null };
1244
+ catch (error) {
1245
+ return { status: "failed", resultText: null, sessionId: null, error: error instanceof Error ? error.message : String(error) };
1246
+ }
1247
+ const { code, stdout, stderr } = executed;
1248
+ const parsed = parseAntigravityOutput(stdout);
1249
+ // A refused run says so in the envelope, not on stderr. Take the envelope's own reason first —
1250
+ // it is the only text that names the cause (a spent allowance and when it refills).
1251
+ const refused = code !== 0 || (parsed.status !== null && parsed.status.toUpperCase() !== "SUCCESS");
1252
+ if (refused) {
1253
+ return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, error: boundedTail(parsed.error || stderr || parsed.resultText || `agy exited with code ${code}`, 20_000) };
1254
+ }
1255
+ // An empty response with a denial on stderr is a refused run: report it instead of an empty reply.
1256
+ const denial = parsed.resultText?.trim() ? null : antigravityDenialNotice(stderr);
1257
+ if (denial) {
1258
+ return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, error: boundedTail(`agy returned no response: ${denial}`, 20_000) };
1259
+ }
1260
+ return { status: "completed", resultText: parsed.resultText, sessionId: parsed.sessionId };
1043
1261
  },
1044
1262
  };
1045
1263
  /**
package/dist/execution.js CHANGED
@@ -74,6 +74,40 @@ function changesRequestedFeedback(summary) {
74
74
  * failure, which is exactly today's behaviour.
75
75
  */
76
76
  export const SUBSCRIPTION_EXHAUSTED_PATTERN = /\b(?:usage|rate|session)[ _-]?limit(?:ed|s)?\b|\b(?:daily|weekly|monthly) limit\b|\bquota (?:exceeded|exhausted|reached)\b|\b(?:http|status|code)\W{0,3}429\b|\b429\b(?=\W{0,3}too many)|too many requests|out of (?:credits|usage)/i;
77
+ /**
78
+ * A vendor naming the account's own allowance as the reason it refused this run.
79
+ *
80
+ * Narrower and louder than SUBSCRIPTION_EXHAUSTED_PATTERN, which decides whether to dark a lane:
81
+ * this one decides what the owner is told. agy 1.1.24 returns `{"status":"ERROR","error":
82
+ * "Individual quota reached. Please upgrade your subscription ... Resets in 165h48m43s."}` with an
83
+ * empty stderr, which had no pattern at all and retried three times in a minute while the owner was
84
+ * told nothing. A spent allowance is a wait, never a retry.
85
+ *
86
+ * Kept identical to the control-plane copy in src/conductor/failures.ts, pinned by
87
+ * test/failure-classification.test.ts — the two ship on different clocks.
88
+ */
89
+ export const PROVIDER_QUOTA_PATTERN = /individual quota reached|quota reached|upgrade your subscription|resets in \d/i;
90
+ export function providerQuotaRefusal(message) {
91
+ return PROVIDER_QUOTA_PATTERN.test(message);
92
+ }
93
+ /**
94
+ * What the owner sees when the account's allowance, not Conduit, ended the run. The reset time is
95
+ * quoted from the vendor's own text when it gave one; nothing is invented when it did not.
96
+ */
97
+ export function providerQuotaFailure(detail) {
98
+ const resets = /resets? in ([^.\n]{1,40})/i.exec(detail);
99
+ return {
100
+ code: "driver_quota_exhausted",
101
+ class: "environment",
102
+ disposition: "hold",
103
+ responsible_party: "computer_operator",
104
+ message: resets
105
+ ? `The agent lane on this computer has spent its subscription allowance. It refills in ${resets[1].trim()}.`
106
+ : "The agent lane on this computer has spent its subscription allowance.",
107
+ next_action: "Wait for the allowance to refill, bring another agent lane online to start sooner, or point this lane's model settings at a model that still has allowance.",
108
+ diagnostic_detail: detail,
109
+ };
110
+ }
77
111
  /**
78
112
  * Does this failure mean the plan is spent, rather than the prompt being wrong?
79
113
  *
@@ -271,10 +305,20 @@ export function observeDriverFuel(config, driverId, fuelSource, result, now = Da
271
305
  }
272
306
  /** Failures that require an operator/configuration change must never burn the remaining attempts. */
273
307
  export function retryableAgentFailure(message) {
308
+ // A spent allowance is the account's window, not a flaky run: every retry inside it fails the
309
+ // same way and burns the attempt budget while the owner is told nothing.
310
+ if (providerQuotaRefusal(message))
311
+ return false;
274
312
  // `cannot enforce` is a driver projection refusing a class it structurally cannot bound (Pi and
275
313
  // observe_network). No number of retries on that lane changes it, and retrying an immutable input
276
314
  // is how one assignment burned ~80 attempts. Fail closed; the message names the drivers that work.
277
- return !/unknown (?:option|argument)|unrecognized (?:option|argument)|not logged in|no login|not authenticated|login required|\bENOENT\b|could not verify the installed CLI|requires local fuel|No Bridge-mapped|cannot enforce|preflight/i.test(message);
315
+ // `took "..." as its prompt` / `Usage of agy` is agy's Go-style flag-parsing refusal (e.g. a
316
+ // driver flag ordering bug feeding it the wrong prompt argument) — an identical arg list fails
317
+ // identically on every retry.
318
+ // `headless mode cannot prompt for` / `was auto-denied` is a soft denial: the CLI refused a tool
319
+ // the run's permission rules did not match. The same rules produce the same denial next time, so
320
+ // it is a driver/permission defect to surface, never a transient to burn attempts on.
321
+ return !/unknown (?:option|argument)|unrecognized (?:option|argument)|not logged in|no login|not authenticated|login required|\bENOENT\b|could not verify the installed CLI|requires local fuel|No Bridge-mapped|cannot enforce|preflight|took "[^"]*" as its prompt|Usage of agy|headless mode cannot prompt for|was auto-denied/i.test(message);
278
322
  }
279
323
  const assignmentSchema = z.object({
280
324
  id: z.string().uuid(),
@@ -1246,7 +1290,12 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
1246
1290
  // Keep the tree until the replay-safe terminal response explicitly says whether diagnosis was
1247
1291
  // queued. A network fault here must not delete the evidence before Bridge can replay terminal.
1248
1292
  retainAttemptWorktree = true;
1249
- const response = await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(agentMessage), idempotency_key: `bridge:fail:${active.attemptId}` } });
1293
+ const response = await queueTerminal(client, taskId, { action: "fail", body: {
1294
+ error: message,
1295
+ retryable: retryableAgentFailure(agentMessage),
1296
+ ...(providerQuotaRefusal(agentMessage) ? { failure: providerQuotaFailure(agentMessage) } : {}),
1297
+ idempotency_key: `bridge:fail:${active.attemptId}`,
1298
+ } });
1250
1299
  retainAttemptWorktree = response.retain_worktree === true;
1251
1300
  console.error(`Assignment ${taskId} failed: ${redactSecrets(message)}`);
1252
1301
  return;
@@ -1414,7 +1463,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
1414
1463
  const message = repairError instanceof Error ? repairError.message : "Delivery report repair failed";
1415
1464
  const detail = `Delivery report repair failed: ${message}`;
1416
1465
  const retryable = retryableAgentFailure(message);
1417
- await queueTerminal(client, taskId, { action: "fail", body: { error: detail, retryable, failure: retryable ? undefined : deliveryReportRepairFailure(detail), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
1466
+ await queueTerminal(client, taskId, { action: "fail", body: { error: detail, retryable, failure: repairTurnFailure(detail, retryable), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
1418
1467
  console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
1419
1468
  return;
1420
1469
  }
@@ -1427,7 +1476,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
1427
1476
  if (repaired.status === "failed") {
1428
1477
  const message = repaired.error ?? "Delivery report repair failed";
1429
1478
  const retryable = retryableAgentFailure(message);
1430
- await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable, failure: retryable ? undefined : deliveryReportRepairFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
1479
+ await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable, failure: repairTurnFailure(message, retryable), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
1431
1480
  console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
1432
1481
  return;
1433
1482
  }
@@ -2083,6 +2132,15 @@ function retainDiagnosticWorktree(response) {
2083
2132
  // receive that decision, so fail safe by retaining the only copy of the failed code.
2084
2133
  return response.retain_worktree === true || response.status === "invalid_lease";
2085
2134
  }
2135
+ /**
2136
+ * A repair turn that died on the account's allowance is not a delivery-report defect. Naming it one
2137
+ * would send the owner to rework a report the agent never got to write.
2138
+ */
2139
+ function repairTurnFailure(detail, retryable) {
2140
+ if (providerQuotaRefusal(detail))
2141
+ return providerQuotaFailure(detail);
2142
+ return retryable ? undefined : deliveryReportRepairFailure(detail);
2143
+ }
2086
2144
  /**
2087
2145
  * The envelope for a delivery report the Bridge could not repair.
2088
2146
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.32",
3
+ "version": "0.16.37",
4
4
  "description": "Conduit Bridge CLI \u2014 join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {