@miraland-labs/conduit-bridge 0.16.106 → 0.16.108

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/execution.js CHANGED
@@ -13,7 +13,7 @@ import { execFile } from "node:child_process";
13
13
  import { promisify } from "node:util";
14
14
  import { buildWorkspaceBrief, ensureCommitAvailable, fetchOrigin, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
15
15
  import { buildSovereignExecutionFacts, bootstrapResultForWorkspace, changedExecutionFact, resumeExecutionFacts, workspaceIsClean } from "./execution-facts.js";
16
- import { bridgeCause, executionFactChangedSignal, verificationRedBaseSignal, verificationScopeConflictSignal } from "./failure-signal.js";
16
+ import { agentOutputCause, boundedDetail, bridgeCause, executionFactChangedSignal, verificationRedBaseSignal, verificationScopeConflictSignal } from "./failure-signal.js";
17
17
  import { forgeTransportFailure } from "./transport-fault.js";
18
18
  import { managedWorkspacePath, switchManagedWorkspace } from "./on-shift-apply.js";
19
19
  import { formatResetMinute, parseQuotaResetAt } from "./quota-reset.js";
@@ -113,7 +113,7 @@ export function providerQuotaFailure(detail) {
113
113
  responsible_party: "computer_operator",
114
114
  message: `The agent lane on this computer has spent its subscription allowance.${refills ? ` ${refills}` : ""}`,
115
115
  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.",
116
- diagnostic_detail: detail,
116
+ diagnostic_detail: boundedDetail(detail),
117
117
  };
118
118
  }
119
119
  /**
@@ -185,7 +185,7 @@ class AgentTurnTimeoutError extends Error {
185
185
  /** Environment faults that must Hold — mirror CP ENVIRONMENT_FAILURES message patterns. */
186
186
  const FINALIZE_ENVIRONMENT_PATTERN = /base[_ ]not[_ ]ancestor|required base commit is not available|source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace|workspace[_ ]head[_ ]changed|workspace[_ ]repository|workspace[_ ]unavailable|driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required|no[_ ]online[_ ]driver|bridge[_ ]preflight|stale bridge|bridge version/i;
187
187
  /** Delivery-report / grant / evidence contract defects (non-retryable rework). */
188
- /** Two contract messages the control plane reads with builders of its own; they carry no code from here. */
188
+ /** Two finalize messages with builders of their own in the twin and the cause table. */
189
189
  const CONTRACT_SCOPE_CONFLICT_MESSAGE = /Agent reported changes outside the approved scope:/i;
190
190
  const VERIFICATION_UNWITNESSABLE_MESSAGE = /verification produced (?:no|insufficient) output/i;
191
191
  const FINALIZE_CONTRACT_PATTERN = /missing required evidence|without the repo_write grant|pull_request_url|Artifact delivery requires|outside the approved scope|head commit|test evidence must include|Met acceptance criteria|Agent report|Agent reported|Agent did not land repository changes|Delivery report|Integration obligation not met|Normative reference unavailable|Normative marker not found/i;
@@ -204,10 +204,12 @@ export function classifyFinalizeFailure(message) {
204
204
  return { retryable: false, error: message };
205
205
  if (FINALIZE_ENVIRONMENT_PATTERN.test(message))
206
206
  return { retryable: false, error: message };
207
- // Bridge names the contract defect it found (K3). A scope conflict keeps its own builder on the
208
- // control plane, which quotes the paths; an unknown throw below stays prose until its own site
209
- // names a code.
207
+ // Bridge names the contract defect it found (K3); an unknown throw below stays prose until its
208
+ // own site names a code.
210
209
  if (FINALIZE_CONTRACT_PATTERN.test(message)) {
210
+ // The scope conflict Holds (contract/hold), and the runner's envelope schema admits only
211
+ // contract/rework from a supplied envelope — sending it would refuse the whole report. It stays
212
+ // prose until the finalize site emits it as a typed signal (K3 step 2b design).
211
213
  const failure = CONTRACT_SCOPE_CONFLICT_MESSAGE.test(message)
212
214
  ? undefined
213
215
  : VERIFICATION_UNWITNESSABLE_MESSAGE.test(message)
@@ -1205,7 +1207,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1205
1207
  error: message,
1206
1208
  retryable,
1207
1209
  failure: retryable
1208
- ? bridgeCause("agent_execution_failed", message)
1210
+ ? bridgeCause(forgeTransportFailure(message) ? "forge_unreachable" : "agent_execution_failed", message)
1209
1211
  : bridgeCause(message.startsWith(NORMATIVE_REF_PREFIX) ? "execution_contract_failed" : "execution_unclassified", message),
1210
1212
  idempotency_key: `bridge:normative-ref:${active.attemptId}`,
1211
1213
  },
@@ -1278,7 +1280,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1278
1280
  responsible_party: "computer_operator",
1279
1281
  message: "Conductor is waiting for a diagnostic lane that can enforce read-only access.",
1280
1282
  next_action: "Bring a Claude Code, Codex, Cursor, or Grok Build lane online on this computer, then Recheck. Do not author compiler constraints for the agent.",
1281
- diagnostic_detail: detail,
1283
+ diagnostic_detail: boundedDetail(detail),
1282
1284
  },
1283
1285
  error: detail,
1284
1286
  retryable: false,
@@ -1450,7 +1452,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1450
1452
  responsible_party: "computer_operator",
1451
1453
  message: "Conductor cannot prove that diagnosis will leave the retained worktree unchanged.",
1452
1454
  next_action: "Restore a readable Git worktree on this computer, then Recheck.",
1453
- diagnostic_detail: detail,
1455
+ diagnostic_detail: boundedDetail(detail),
1454
1456
  },
1455
1457
  error: detail,
1456
1458
  retryable: false,
@@ -1653,7 +1655,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1653
1655
  ? "The selected diagnostic lane wrote to a worktree it was only allowed to inspect."
1654
1656
  : "Conductor cannot prove that diagnosis left the retained worktree unchanged.",
1655
1657
  next_action: "Bring a diagnostic lane online that enforces read-only repository access, then Recheck.",
1656
- diagnostic_detail: detail,
1658
+ diagnostic_detail: boundedDetail(detail),
1657
1659
  },
1658
1660
  error: detail,
1659
1661
  retryable: false,
@@ -1666,11 +1668,15 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1666
1668
  }
1667
1669
  if (result.status === "failed") {
1668
1670
  const message = result.error ?? "Read-only diagnosis failed";
1671
+ // A spent allowance keeps its prose reading here (the control plane quotes the reset clock
1672
+ // from it); a computer fact in the CLI's words is named by code, as on the authoring path.
1673
+ const diagnosisCause = providerQuotaRefusal(message) ? null : agentOutputCause(message);
1669
1674
  const response = await queueTerminal(client, taskId, {
1670
1675
  action: "fail",
1671
1676
  body: {
1672
1677
  error: message,
1673
1678
  retryable: retryableAgentFailure(message),
1679
+ ...(diagnosisCause ? { failure: bridgeCause(diagnosisCause, message) } : {}),
1674
1680
  idempotency_key: `bridge:diagnosis-fail:${active.attemptId}`,
1675
1681
  },
1676
1682
  });
@@ -1750,6 +1756,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1750
1756
  : null;
1751
1757
  const abortPrefix = agentModelAbortPrefix(selection.model);
1752
1758
  const modelAborted = agentModelAborted(`${agentMessage}\n${result.signal?.stderr_tail ?? ""}`);
1759
+ const agentCause = agentOutputCause(agentMessage);
1753
1760
  // T52: the epoch's second abort is not a retryable model swap — the first one already bought
1754
1761
  // the T34 retry, and a tier that aborts twice runs for the whole attempt budget (R12: three
1755
1762
  // "Agent Looping Detected" runs on the cursor CLI default). Hold on the tier instead.
@@ -1781,8 +1788,10 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1781
1788
  const response = await queueTerminal(client, taskId, { action: "fail", body: {
1782
1789
  error: message,
1783
1790
  retryable: bindingRequired ? false : retryableAgentFailure(agentMessage),
1784
- // The abort is Bridge's own reading of the vendor's words, so Bridge names the cause; a spent
1785
- // allowance, read the same way, wins over it as it always has.
1791
+ // Bridge's own readings of the vendor's words name the cause, in the order the control plane
1792
+ // always applied: a computer fact (sign-in, incompatible or missing CLI), then a model abort,
1793
+ // then a spent allowance, each later one winning.
1794
+ ...(!bindingRequired && !verificationDetail && agentCause ? { failure: bridgeCause(agentCause, message) } : {}),
1786
1795
  ...(modelAborted && !bindingRequired && !verificationDetail ? { failure: bridgeCause("agent_model_abort", message) } : {}),
1787
1796
  ...(providerQuotaRefusal(agentMessage) ? { failure: providerQuotaFailure(agentMessage) } : {}),
1788
1797
  ...(signal ? { signal } : {}),
@@ -2950,7 +2959,7 @@ function deliveryReportRepairFailure(detail) {
2950
2959
  responsible_party: "conduit",
2951
2960
  message: "Conduit could not prepare a valid delivery report from the completed agent run.",
2952
2961
  next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
2953
- diagnostic_detail: detail,
2962
+ diagnostic_detail: boundedDetail(detail),
2954
2963
  };
2955
2964
  }
2956
2965
  /**
@@ -3085,7 +3094,7 @@ export async function flushTerminal(client, taskId) {
3085
3094
  responsible_party: "conduit",
3086
3095
  message: "The completed delivery report did not match the owner-approved acceptance criteria.",
3087
3096
  next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
3088
- diagnostic_detail: detail,
3097
+ diagnostic_detail: boundedDetail(detail),
3089
3098
  },
3090
3099
  idempotency_key: `bridge:delivery-rejected:${active.attemptId}`,
3091
3100
  },
@@ -85,6 +85,9 @@ export const failureSignalSchema = z.object({
85
85
  */
86
86
  export const BRIDGE_CAUSES = {
87
87
  workspace_repository_mismatch: { class: "environment", disposition: "hold", responsible_party: "computer_operator", message: "This checkout does not match the work package repository.", next_action: "Run Bridge from the matching repository checkout, then recheck readiness." },
88
+ driver_not_authenticated: { class: "environment", disposition: "hold", responsible_party: "computer_operator", message: "The selected agent CLI is not signed in.", next_action: "Sign in to the agent CLI on this computer, then recheck readiness." },
89
+ driver_incompatible: { class: "environment", disposition: "hold", responsible_party: "computer_operator", message: "The installed agent CLI is incompatible with this Bridge driver.", next_action: "Update Bridge or the agent CLI to a compatible version, then recheck readiness." },
90
+ driver_missing: { class: "environment", disposition: "hold", responsible_party: "computer_operator", message: "The selected agent CLI is not available to Bridge.", next_action: "Install the CLI or repair the background service PATH, then recheck readiness." },
88
91
  workspace_unavailable: { class: "environment", disposition: "hold", responsible_party: "computer_operator", message: "Bridge cannot read the configured workspace.", next_action: "Restore access to the workspace, then recheck readiness." },
89
92
  workspace_head_changed: { class: "environment", disposition: "hold", responsible_party: "computer_operator", message: "The checkout changed after Conduit prepared the assignment.", next_action: "Let the checkout settle, then recheck readiness." },
90
93
  base_not_ancestor: { class: "environment", disposition: "hold", responsible_party: "computer_operator", message: "Conduit could not prepare the required project version on this computer.", next_action: "No Git work is required from the product owner. Keep the computer and Bridge online, then try again so Bridge can refresh the project. If the hold returns, the computer operator should check Bridge's repository access." },
@@ -103,9 +106,79 @@ export function isBridgeCauseCode(value) {
103
106
  return Object.prototype.hasOwnProperty.call(BRIDGE_CAUSES, value);
104
107
  }
105
108
  /** The envelope for a cause Bridge names by code, with the run's own words as the detail. */
109
+ /**
110
+ * The control plane's envelope schema bounds `diagnostic_detail` at 20,000 characters and refuses
111
+ * the whole terminal report otherwise: the run's words are cut here, never the report.
112
+ */
113
+ export function boundedDetail(text) {
114
+ const trimmed = text.trim();
115
+ return trimmed.length > 20_000 ? `${trimmed.slice(0, 19_999)}…` : trimmed;
116
+ }
106
117
  export function bridgeCause(code, detail) {
107
118
  const cause = BRIDGE_CAUSES[code];
108
- return { code, ...cause, ...(detail?.trim() ? { diagnostic_detail: detail.trim() } : {}) };
119
+ const text = detail ? boundedDetail(detail) : "";
120
+ return { code, ...cause, ...(text ? { diagnostic_detail: text } : {}) };
121
+ }
122
+ /**
123
+ * Bridge's reading of the agent CLI's own words for three computer facts: not signed in, an
124
+ * incompatible CLI, a CLI that is not there. Vendor knowledge, held once, beside the other vendor
125
+ * patterns above; the control plane reads the code, never the sentence.
126
+ *
127
+ * Deliberately not bare `401`/`unauthorized`: those appear constantly in ordinary test and HTTP
128
+ * output, and matching them would reclassify a real verify failure as an environment Hold.
129
+ */
130
+ const DRIVER_NOT_AUTHENTICATED = /driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required|no api key|api key not found|missing api key|invalid api key|use \/login|\b401\b[^\n]{0,80}(?:api key|auth|credential|token)|(?:api key|auth|credential|token)[^\n]{0,80}\b401\b/i;
131
+ const DRIVER_INCOMPATIBLE = /unknown (?:option|argument)|unrecognized (?:option|argument)|could not verify the installed cli|took "[^"]*" as its prompt|usage of agy|headless mode cannot prompt for|was auto-denied/i;
132
+ const DRIVER_MISSING = /\benoent\b|agent cli is missing|driver[_ ]missing/i;
133
+ export function agentOutputCause(output) {
134
+ if (DRIVER_NOT_AUTHENTICATED.test(output))
135
+ return "driver_not_authenticated";
136
+ if (DRIVER_INCOMPATIBLE.test(output))
137
+ return "driver_incompatible";
138
+ if (DRIVER_MISSING.test(output))
139
+ return "driver_missing";
140
+ return null;
141
+ }
142
+ /**
143
+ * Bridge `validateChangeScope` names the paths the approved change scope does not contain. The
144
+ * paths follow the colon, on one line, comma-separated.
145
+ */
146
+ const CONTRACT_SCOPE_CONFLICT_PATTERN = /Agent reported changes outside the approved scope:[ \t]*(.+)/i;
147
+ /**
148
+ * The approved change scope, when Bridge names it on its own line after the offending paths
149
+ * (`validateChangeScope` and the dry run's scope refusal both do, since T31). `.` in the paths
150
+ * pattern above never crosses this line, so the two extractions cannot collide.
151
+ */
152
+ const APPROVED_CHANGE_SCOPE_LINE = /^approved change scope:[ \t]*(.+)$/im;
153
+ /** The offered scope, bounded and ready to append after a colon-terminated sentence. */
154
+ export function approvedScopeNote(detail) {
155
+ const scope = APPROVED_CHANGE_SCOPE_LINE.exec(detail)?.[1]?.trim();
156
+ if (!scope)
157
+ return "";
158
+ return `; approved change scope: ${scope.length > 1_500 ? `${scope.slice(0, 1_499)}…` : scope}`;
159
+ }
160
+ /** The work package asks for paths its own change scope does not contain. */
161
+ export const CONTRACT_SCOPE_CONFLICT_CODE = "contract_scope_conflict";
162
+ /**
163
+ * The defect is in the work package, not in the run: the contract told the engine to write paths
164
+ * the approved change scope does not contain. No agent, no diagnosis and no retry can widen a
165
+ * change scope, so this failure never enters the repair loop. It Holds on the initiative author
166
+ * (`hold`, not `rework`: a rework disposition seeds the repair outbox), and the Hold stays open
167
+ * until a plan revision replaces the work package.
168
+ */
169
+ export function contractScopeConflictFailure(detail) {
170
+ const paths = CONTRACT_SCOPE_CONFLICT_PATTERN.exec(detail)?.[1]?.trim() ?? "";
171
+ // The offending paths are quoted verbatim, inside the envelope message bound.
172
+ const named = paths.length > 1_500 ? `${paths.slice(0, 1_499)}…` : paths;
173
+ return {
174
+ code: CONTRACT_SCOPE_CONFLICT_CODE,
175
+ class: "contract",
176
+ disposition: "hold",
177
+ responsible_party: "initiative_author",
178
+ message: `This work package had to write paths its approved change scope does not contain: ${named}${approvedScopeNote(detail)}`,
179
+ next_action: "Open revision and either add these paths to the change scope or make verification write to a temporary directory.",
180
+ diagnostic_detail: boundedDetail(detail),
181
+ };
109
182
  }
110
183
  /** A reset time as a person reads it: "2026-09-10 09:36 UTC". Every reset is stored ISO in UTC. */
111
184
  function resetClock(resetsAt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.106",
3
+ "version": "0.16.108",
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": {