@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 +23 -14
- package/dist/failure-signal.js +74 -1
- package/package.json +1 -1
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
|
|
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)
|
|
208
|
-
//
|
|
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
|
-
//
|
|
1785
|
-
//
|
|
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
|
},
|
package/dist/failure-signal.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": {
|