@miraland-labs/conduit-bridge 0.16.107 → 0.16.109
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 +59 -14
- package/dist/failure-signal.js +100 -4
- package/dist/investigation.js +59 -7
- 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";
|
|
@@ -25,7 +25,7 @@ import { ensureLandCommit } from "./ensure-land-commit.js";
|
|
|
25
25
|
import { AGENT_NO_LAND_COMMIT_PREFIX, AgentNoLandCommitError, agentNoLandCommitMessage, requiresLandCommit } from "./land-contract.js";
|
|
26
26
|
import { agentClaimsRepositoryWork, claimsVsGitMismatch, isLandTreeEmpty, reportClaimsRepositoryWork, } from "./land-git-reality.js";
|
|
27
27
|
const execFileAsync = promisify(execFile);
|
|
28
|
-
import { captureVerificationFailure, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationGateRanNothingError } from "./ensure-test-evidence.js";
|
|
28
|
+
import { boundedTail, captureVerificationFailure, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationGateRanNothingError } from "./ensure-test-evidence.js";
|
|
29
29
|
import { ensureChangeEvidence } from "./ensure-change-evidence.js";
|
|
30
30
|
import { ensureNormativeEvidence } from "./ensure-normative-evidence.js";
|
|
31
31
|
import { changedPathsSince, compareWorktreeState, diffStatSince, worktreeStateFingerprint, worktreeWrittenPaths } from "./git-witness.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)
|
|
@@ -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 } : {}),
|
|
@@ -2863,6 +2872,42 @@ export async function verificationScopeDryRun(input) {
|
|
|
2863
2872
|
: written.filter((path) => !input.changeScope.some((scope) => pathMatchesScope(path, scope)));
|
|
2864
2873
|
return { ranNothing, redBase, outsideScope };
|
|
2865
2874
|
}
|
|
2875
|
+
/** Prompt and failure lines keep a short tail; the investigation brief caps each failure at 1,000. */
|
|
2876
|
+
const DELIVERY_VERIFICATION_GATE_TAIL_CHARS = 800;
|
|
2877
|
+
/**
|
|
2878
|
+
* Run the T191 gate set in the verification worktree at the delivered commit: declared
|
|
2879
|
+
* `.conduit/verification` ∪ the commands the criteria name, else the discovered set. One at a
|
|
2880
|
+
* time, same bounded runner as the pre-agent dry run. Writes the gates made are discarded so the
|
|
2881
|
+
* agent still reads the delivered tree.
|
|
2882
|
+
*/
|
|
2883
|
+
export async function runDeliveryVerificationGates(input) {
|
|
2884
|
+
const commands = verificationEvidenceCommands([...input.verificationCommands], input.declaredVerificationCommands ?? [], input.acceptance ?? []);
|
|
2885
|
+
const run = input.runCommand ?? runBoundedVerificationCommand;
|
|
2886
|
+
const results = [];
|
|
2887
|
+
for (const command of commands) {
|
|
2888
|
+
try {
|
|
2889
|
+
const result = await run(command, input.workspace);
|
|
2890
|
+
results.push({
|
|
2891
|
+
command,
|
|
2892
|
+
exitCode: result.code,
|
|
2893
|
+
stdoutTail: boundedTail(result.stdout, DELIVERY_VERIFICATION_GATE_TAIL_CHARS),
|
|
2894
|
+
stderrTail: boundedTail(result.stderr, DELIVERY_VERIFICATION_GATE_TAIL_CHARS),
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
catch (error) {
|
|
2898
|
+
// A gate that could not run is a failed gate: record it so settlement still names it.
|
|
2899
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2900
|
+
results.push({
|
|
2901
|
+
command,
|
|
2902
|
+
exitCode: -1,
|
|
2903
|
+
stdoutTail: "",
|
|
2904
|
+
stderrTail: boundedTail(message, DELIVERY_VERIFICATION_GATE_TAIL_CHARS),
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
await discardWorktreeWrites(input.workspace);
|
|
2909
|
+
return results;
|
|
2910
|
+
}
|
|
2866
2911
|
/** Return the worktree to its commit: tracked files restored, new files removed, ignored files kept. */
|
|
2867
2912
|
async function discardWorktreeWrites(worktree) {
|
|
2868
2913
|
const options = { timeout: 60_000, maxBuffer: 2_000_000 };
|
|
@@ -2950,7 +2995,7 @@ function deliveryReportRepairFailure(detail) {
|
|
|
2950
2995
|
responsible_party: "conduit",
|
|
2951
2996
|
message: "Conduit could not prepare a valid delivery report from the completed agent run.",
|
|
2952
2997
|
next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
|
|
2953
|
-
diagnostic_detail: detail,
|
|
2998
|
+
diagnostic_detail: boundedDetail(detail),
|
|
2954
2999
|
};
|
|
2955
3000
|
}
|
|
2956
3001
|
/**
|
|
@@ -3085,7 +3130,7 @@ export async function flushTerminal(client, taskId) {
|
|
|
3085
3130
|
responsible_party: "conduit",
|
|
3086
3131
|
message: "The completed delivery report did not match the owner-approved acceptance criteria.",
|
|
3087
3132
|
next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
|
|
3088
|
-
diagnostic_detail: detail,
|
|
3133
|
+
diagnostic_detail: boundedDetail(detail),
|
|
3089
3134
|
},
|
|
3090
3135
|
idempotency_key: `bridge:delivery-rejected:${active.attemptId}`,
|
|
3091
3136
|
},
|
package/dist/failure-signal.js
CHANGED
|
@@ -75,6 +75,32 @@ export const failureSignalSchema = z.object({
|
|
|
75
75
|
*/
|
|
76
76
|
model: z.string().max(200).optional(),
|
|
77
77
|
});
|
|
78
|
+
/**
|
|
79
|
+
* The class–disposition pairs a classified failure may carry. Disposition is a function of the
|
|
80
|
+
* cause, not of class: the six canonical pairs plus `contract`/`hold` (the work package is the
|
|
81
|
+
* defect; a rework would spend repair on code that is not at fault) and `environment`/`retry`
|
|
82
|
+
* (the computer hiccupped; the next lane runs it again).
|
|
83
|
+
*
|
|
84
|
+
* Declared once so the envelope schema and both twins share one list. `ClassifiedFailure` stays a
|
|
85
|
+
* free pair; this list is the boundary check.
|
|
86
|
+
*/
|
|
87
|
+
export const ADMITTED_FAILURE_PAIRS = [
|
|
88
|
+
["transient", "retry"],
|
|
89
|
+
["environment", "hold"],
|
|
90
|
+
["environment", "retry"],
|
|
91
|
+
["contract", "rework"],
|
|
92
|
+
["contract", "hold"],
|
|
93
|
+
["quality", "rework"],
|
|
94
|
+
["authority", "decision"],
|
|
95
|
+
["platform", "stop"],
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* Whether this class and disposition are one of the admitted pairs. The envelope schema calls it
|
|
99
|
+
* so a supplied envelope cannot carry a pairing the builders never emit.
|
|
100
|
+
*/
|
|
101
|
+
export function isAdmittedFailurePair(failureClass, disposition) {
|
|
102
|
+
return ADMITTED_FAILURE_PAIRS.some(([admittedClass, admittedDisposition]) => admittedClass === failureClass && admittedDisposition === disposition);
|
|
103
|
+
}
|
|
78
104
|
/**
|
|
79
105
|
* The causes Bridge names itself — before the agent starts, or while finishing its report — as one
|
|
80
106
|
* table keyed by code, so Bridge sends the envelope and the control plane validates it (K3).
|
|
@@ -85,6 +111,9 @@ export const failureSignalSchema = z.object({
|
|
|
85
111
|
*/
|
|
86
112
|
export const BRIDGE_CAUSES = {
|
|
87
113
|
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." },
|
|
114
|
+
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." },
|
|
115
|
+
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." },
|
|
116
|
+
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
117
|
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
118
|
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
119
|
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,12 +132,79 @@ export function isBridgeCauseCode(value) {
|
|
|
103
132
|
return Object.prototype.hasOwnProperty.call(BRIDGE_CAUSES, value);
|
|
104
133
|
}
|
|
105
134
|
/** The envelope for a cause Bridge names by code, with the run's own words as the detail. */
|
|
135
|
+
/**
|
|
136
|
+
* The control plane's envelope schema bounds `diagnostic_detail` at 20,000 characters and refuses
|
|
137
|
+
* the whole terminal report otherwise: the run's words are cut here, never the report.
|
|
138
|
+
*/
|
|
139
|
+
export function boundedDetail(text) {
|
|
140
|
+
const trimmed = text.trim();
|
|
141
|
+
return trimmed.length > 20_000 ? `${trimmed.slice(0, 19_999)}…` : trimmed;
|
|
142
|
+
}
|
|
106
143
|
export function bridgeCause(code, detail) {
|
|
107
144
|
const cause = BRIDGE_CAUSES[code];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
145
|
+
const text = detail ? boundedDetail(detail) : "";
|
|
146
|
+
return { code, ...cause, ...(text ? { diagnostic_detail: text } : {}) };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Bridge's reading of the agent CLI's own words for three computer facts: not signed in, an
|
|
150
|
+
* incompatible CLI, a CLI that is not there. Vendor knowledge, held once, beside the other vendor
|
|
151
|
+
* patterns above; the control plane reads the code, never the sentence.
|
|
152
|
+
*
|
|
153
|
+
* Deliberately not bare `401`/`unauthorized`: those appear constantly in ordinary test and HTTP
|
|
154
|
+
* output, and matching them would reclassify a real verify failure as an environment Hold.
|
|
155
|
+
*/
|
|
156
|
+
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;
|
|
157
|
+
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;
|
|
158
|
+
const DRIVER_MISSING = /\benoent\b|agent cli is missing|driver[_ ]missing/i;
|
|
159
|
+
export function agentOutputCause(output) {
|
|
160
|
+
if (DRIVER_NOT_AUTHENTICATED.test(output))
|
|
161
|
+
return "driver_not_authenticated";
|
|
162
|
+
if (DRIVER_INCOMPATIBLE.test(output))
|
|
163
|
+
return "driver_incompatible";
|
|
164
|
+
if (DRIVER_MISSING.test(output))
|
|
165
|
+
return "driver_missing";
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Bridge `validateChangeScope` names the paths the approved change scope does not contain. The
|
|
170
|
+
* paths follow the colon, on one line, comma-separated.
|
|
171
|
+
*/
|
|
172
|
+
const CONTRACT_SCOPE_CONFLICT_PATTERN = /Agent reported changes outside the approved scope:[ \t]*(.+)/i;
|
|
173
|
+
/**
|
|
174
|
+
* The approved change scope, when Bridge names it on its own line after the offending paths
|
|
175
|
+
* (`validateChangeScope` and the dry run's scope refusal both do, since T31). `.` in the paths
|
|
176
|
+
* pattern above never crosses this line, so the two extractions cannot collide.
|
|
177
|
+
*/
|
|
178
|
+
const APPROVED_CHANGE_SCOPE_LINE = /^approved change scope:[ \t]*(.+)$/im;
|
|
179
|
+
/** The offered scope, bounded and ready to append after a colon-terminated sentence. */
|
|
180
|
+
export function approvedScopeNote(detail) {
|
|
181
|
+
const scope = APPROVED_CHANGE_SCOPE_LINE.exec(detail)?.[1]?.trim();
|
|
182
|
+
if (!scope)
|
|
183
|
+
return "";
|
|
184
|
+
return `; approved change scope: ${scope.length > 1_500 ? `${scope.slice(0, 1_499)}…` : scope}`;
|
|
185
|
+
}
|
|
186
|
+
/** The work package asks for paths its own change scope does not contain. */
|
|
187
|
+
export const CONTRACT_SCOPE_CONFLICT_CODE = "contract_scope_conflict";
|
|
188
|
+
/**
|
|
189
|
+
* The defect is in the work package, not in the run: the contract told the engine to write paths
|
|
190
|
+
* the approved change scope does not contain. No agent, no diagnosis and no retry can widen a
|
|
191
|
+
* change scope, so this failure never enters the repair loop. It Holds on the initiative author
|
|
192
|
+
* (`hold`, not `rework`: a rework disposition seeds the repair outbox), and the Hold stays open
|
|
193
|
+
* until a plan revision replaces the work package.
|
|
194
|
+
*/
|
|
195
|
+
export function contractScopeConflictFailure(detail) {
|
|
196
|
+
const paths = CONTRACT_SCOPE_CONFLICT_PATTERN.exec(detail)?.[1]?.trim() ?? "";
|
|
197
|
+
// The offending paths are quoted verbatim, inside the envelope message bound.
|
|
198
|
+
const named = paths.length > 1_500 ? `${paths.slice(0, 1_499)}…` : paths;
|
|
199
|
+
return {
|
|
200
|
+
code: CONTRACT_SCOPE_CONFLICT_CODE,
|
|
201
|
+
class: "contract",
|
|
202
|
+
disposition: "hold",
|
|
203
|
+
responsible_party: "initiative_author",
|
|
204
|
+
message: `This work package had to write paths its approved change scope does not contain: ${named}${approvedScopeNote(detail)}`,
|
|
205
|
+
next_action: "Open revision and either add these paths to the change scope or make verification write to a temporary directory.",
|
|
206
|
+
diagnostic_detail: boundedDetail(detail),
|
|
207
|
+
};
|
|
112
208
|
}
|
|
113
209
|
/** A reset time as a person reads it: "2026-09-10 09:36 UTC". Every reset is stored ISO in UTC. */
|
|
114
210
|
function resetClock(resetsAt) {
|
package/dist/investigation.js
CHANGED
|
@@ -11,10 +11,9 @@ import { createAttemptWorktree, removeAttemptWorktree } from "./attempt-worktree
|
|
|
11
11
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } from "./brief.js";
|
|
12
12
|
import { headCommitOrNull } from "./git.js";
|
|
13
13
|
import { redactSecrets } from "./config.js";
|
|
14
|
-
import { learnDriverFuel, resolveAssignmentModel } from "./execution.js";
|
|
14
|
+
import { learnDriverFuel, resolveAssignmentModel, changedPathsSince, runDeliveryVerificationGates } from "./execution.js";
|
|
15
15
|
import { DRIVERS, extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
|
|
16
16
|
import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
17
|
-
import { changedPathsSince } from "./execution.js";
|
|
18
17
|
import { diffStatSince } from "./git-witness.js";
|
|
19
18
|
import { switchManagedWorkspace } from "./on-shift-apply.js";
|
|
20
19
|
import { execFile } from "node:child_process";
|
|
@@ -36,6 +35,8 @@ export const investigationAssignmentSchema = z.object({
|
|
|
36
35
|
/** T122: present on a delivery verification — the contract base the delivered head was built on. */
|
|
37
36
|
base_commit: z.string().nullable().optional().default(null),
|
|
38
37
|
source_attempt_id: z.string().uuid().nullable().optional().default(null),
|
|
38
|
+
/** Delivery verification: the task's acceptance strings. Absent or [] on a plain investigation. */
|
|
39
|
+
acceptance: z.array(z.string().trim().min(1).max(4_000)).max(20).optional().default([]),
|
|
39
40
|
});
|
|
40
41
|
/**
|
|
41
42
|
* Enforcement is a vendor's claim; verification is ours. Both run on every investigation — this only
|
|
@@ -95,7 +96,7 @@ export function buildInvestigationPrompt(assignment, commit, context) {
|
|
|
95
96
|
}
|
|
96
97
|
/**
|
|
97
98
|
* T122: the Conductor's own check of a delivery, done the way a careful reviewer does it by hand —
|
|
98
|
-
*
|
|
99
|
+
* read the gates Bridge already ran at the delivered head, read the diff, judge every criterion. The
|
|
99
100
|
* verdict per criterion is the product; prose without verdicts is an unusable brief.
|
|
100
101
|
*/
|
|
101
102
|
export function buildDeliveryVerificationPrompt(assignment, commit, context) {
|
|
@@ -113,10 +114,11 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
|
|
|
113
114
|
"You are Conductor's independent delivery verification. A coding agent delivered this commit against approved acceptance criteria; you check it the way a careful reviewer would, and you never take the agent's word for anything.",
|
|
114
115
|
"You have repo_read and test_run only. Do not edit, create, delete, format, commit, branch, push, install, or change any file.",
|
|
115
116
|
diff,
|
|
116
|
-
|
|
117
|
+
formatGatesRunBlock(context.gatesRun ?? []),
|
|
118
|
+
"Bridge already ran the gates at this commit. The GATES RUN block is the only gate fact — use those exit codes and output tails; do not choose which commands count as gates. You may still run a REGISTERED COMMAND to read more. Run nothing that is not registered. Never start a server, deploy, or call a production system.",
|
|
117
119
|
// T132: three verifications ended at the time limit with no brief. Order the work so the brief
|
|
118
120
|
// exists before the budget does not.
|
|
119
|
-
"Order of work:
|
|
121
|
+
"Order of work: read the GATES RUN facts first, then the changed files that matter, then write the brief. You may run a registered command to read more, one at a time and never two at once (a test suite that rewrites a source file and restores it races against itself when run concurrently, and a changed tree voids your run). Your time is limited: a brief with `unclear` verdicts for what you did not reach is the required outcome; a run that ends without a brief settles nothing.",
|
|
120
122
|
"Judge each ACCEPTANCE criterion by its key: supported only when you saw the code or the command output that meets it; unsupported when you saw it is not met, with the file or output that shows it; unclear when this commit and these commands cannot settle it. A criterion about wording in a pull request or a report is unclear here — you cannot see those.",
|
|
121
123
|
"Do not quote secrets, credentials, tokens, or file bodies. Name paths, symbols, and command output lines instead.",
|
|
122
124
|
"",
|
|
@@ -130,6 +132,41 @@ export function buildDeliveryVerificationPrompt(assignment, commit, context) {
|
|
|
130
132
|
"criteria has one entry per ACCEPTANCE key. Close the fence and add no prose after it.",
|
|
131
133
|
].join("\n");
|
|
132
134
|
}
|
|
135
|
+
function formatGatesRunBlock(gates) {
|
|
136
|
+
const header = "GATES RUN (Bridge ran these at the pinned commit before you started; these are the only gate facts)";
|
|
137
|
+
if (!gates.length)
|
|
138
|
+
return `${header}\n- none`;
|
|
139
|
+
return `${header}\n${gates.map((gate) => {
|
|
140
|
+
const tail = gate.stderrTail.trim() || gate.stdoutTail.trim();
|
|
141
|
+
return tail
|
|
142
|
+
? `- ${gate.command} — exit ${gate.exitCode}\n ${tail.replace(/\n/g, "\n ")}`
|
|
143
|
+
: `- ${gate.command} — exit ${gate.exitCode}`;
|
|
144
|
+
}).join("\n")}`;
|
|
145
|
+
}
|
|
146
|
+
function gateFailureLine(tail) {
|
|
147
|
+
const lines = tail.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
148
|
+
const marked = lines.find((line) => /FAIL|Error|error|failed|✗/.test(line));
|
|
149
|
+
const chosen = marked ?? lines.at(-1) ?? tail;
|
|
150
|
+
return chosen.length > 1_000 ? chosen.slice(0, 1_000) : chosen;
|
|
151
|
+
}
|
|
152
|
+
function gateFailureFact(gate) {
|
|
153
|
+
const tail = gate.stderrTail.trim() || gate.stdoutTail.trim() || "exited non-zero";
|
|
154
|
+
return boundedTail(`${gate.command} — exit ${gate.exitCode}: ${gateFailureLine(tail)}`, 1_000);
|
|
155
|
+
}
|
|
156
|
+
function settleCommandsRun(gates, reported) {
|
|
157
|
+
const out = [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
for (const command of [...gates.map((gate) => gate.command), ...reported]) {
|
|
160
|
+
const trimmed = command.trim();
|
|
161
|
+
if (!trimmed || seen.has(trimmed))
|
|
162
|
+
continue;
|
|
163
|
+
seen.add(trimmed);
|
|
164
|
+
out.push(trimmed);
|
|
165
|
+
if (out.length === 10)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
133
170
|
/** One message for every unusable answer, so the retry decision reads it rather than guessing. */
|
|
134
171
|
export const UNUSABLE_BRIEF = "Investigation returned no usable brief";
|
|
135
172
|
/** T130: how much of an unusable reply travels with the failure, so the next run can be diagnosed. */
|
|
@@ -303,9 +340,20 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
303
340
|
: undefined;
|
|
304
341
|
// T126: the prompt names the commands of the repository this lane opened — not the control
|
|
305
342
|
// plane's idea of them — and carries the diff stat Bridge computed, since the lane cannot run git.
|
|
343
|
+
const deliveryVerification = isDeliveryVerification(assignment);
|
|
344
|
+
const gatesRun = deliveryVerification
|
|
345
|
+
? await runDeliveryVerificationGates({
|
|
346
|
+
workspace: attemptWorkspace,
|
|
347
|
+
verificationCommands: liveBrief?.verification ?? [],
|
|
348
|
+
declaredVerificationCommands: liveBrief?.declared_verification ?? [],
|
|
349
|
+
acceptance: assignment.acceptance ?? [],
|
|
350
|
+
runCommand: options.runVerificationCommand,
|
|
351
|
+
})
|
|
352
|
+
: [];
|
|
306
353
|
const context = {
|
|
307
354
|
verificationCommands: liveBrief?.verification ?? [],
|
|
308
355
|
diffStat: assignment.base_commit ? await diffStatSince(attemptWorkspace, assignment.base_commit) : null,
|
|
356
|
+
gatesRun,
|
|
309
357
|
};
|
|
310
358
|
// T154: the lane resolves its model the way the authoring lane does. Without it Claude Code ran
|
|
311
359
|
// at its CLI default, which Conduit's /v1 rejects as an unknown alias (hosted-1, 2026-09-10:
|
|
@@ -354,7 +402,9 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
354
402
|
verification: observed.verification,
|
|
355
403
|
limitations: observed.limitations,
|
|
356
404
|
criteria: observed.criteria,
|
|
357
|
-
command_failures:
|
|
405
|
+
command_failures: deliveryVerification
|
|
406
|
+
? gatesRun.filter((gate) => gate.exitCode !== 0).map(gateFailureFact).slice(0, 10)
|
|
407
|
+
: observed.command_failures,
|
|
358
408
|
witness: {
|
|
359
409
|
repository_fingerprint: assignment.repository_fingerprint ?? liveRepository ?? "unknown",
|
|
360
410
|
commit,
|
|
@@ -362,7 +412,9 @@ export async function executeNextInvestigation(client, config, workspace, brief,
|
|
|
362
412
|
bounded_by: boundedByForDriver(driverId),
|
|
363
413
|
files_read: observed.files_read,
|
|
364
414
|
bytes_read: observed.bytes_read,
|
|
365
|
-
commands_run:
|
|
415
|
+
commands_run: deliveryVerification
|
|
416
|
+
? settleCommandsRun(gatesRun, observed.commands_run)
|
|
417
|
+
: observed.commands_run,
|
|
366
418
|
},
|
|
367
419
|
},
|
|
368
420
|
});
|
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.109",
|
|
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": {
|