@miraland-labs/conduit-bridge 0.16.108 → 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 CHANGED
@@ -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";
@@ -2872,6 +2872,42 @@ export async function verificationScopeDryRun(input) {
2872
2872
  : written.filter((path) => !input.changeScope.some((scope) => pathMatchesScope(path, scope)));
2873
2873
  return { ranNothing, redBase, outsideScope };
2874
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
+ }
2875
2911
  /** Return the worktree to its commit: tracked files restored, new files removed, ignored files kept. */
2876
2912
  async function discardWorktreeWrites(worktree) {
2877
2913
  const options = { timeout: 60_000, maxBuffer: 2_000_000 };
@@ -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).
@@ -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
- * run the registered commands at the delivered head, read the diff, judge every criterion. The
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
- "Run every REGISTERED COMMAND the criteria name, exactly as registered, and read its real output. Run nothing that is not registered. Never start a server, deploy, or call a production system.",
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: run the registered commands first, 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), then read the changed files that matter, then write the brief. 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.",
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: observed.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: observed.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.108",
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": {