@miraland-labs/conduit-bridge 0.16.147 → 0.16.149

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.
@@ -208,7 +208,7 @@ export async function captureVerificationFailure(input) {
208
208
  // Same rule as the pre-delivery gate: this is the diagnosis a repair brief is written from, so
209
209
  // it must keep the end. It carried its own head-truncating copy of that logic until the two were
210
210
  // shown to be the same bug.
211
- const text = boundedFailureDetail(command, result.stdout.trim() ? result.stdout.trim().split("\n") : [], result.stderr.trim() ? result.stderr.trim().split("\n") : [], result.code, DIAGNOSIS_DETAIL_MAX_CHARS);
211
+ const text = boundedFailureDetail(command, outputLines(result), result.code, DIAGNOSIS_DETAIL_MAX_CHARS);
212
212
  if (text.trim())
213
213
  return { command, detail: text };
214
214
  }
@@ -232,18 +232,49 @@ const TAIL_HEAD_CHARS = 200;
232
232
  *
233
233
  * Returns the input unchanged when it already fits, so nothing short is ever decorated.
234
234
  */
235
- export function boundedDetailLines(lines, maxLines) {
235
+ export function boundedDetailLines(lines, maxLines, identifiers = []) {
236
236
  if (lines.length <= maxLines)
237
237
  return lines;
238
238
  const head = Math.min(3, Math.max(0, maxLines - 2));
239
239
  const tail = maxLines - head - 1;
240
- const dropped = lines.length - head - tail;
240
+ const middle = lines.slice(head, lines.length - tail);
241
+ // A criterion such as "its output shows privileged_wallet_holds_the_plan_without_payment as ok"
242
+ // is proved by one line in the middle of a long suite, and a reviewer must not infer it from the
243
+ // summary. So the lines that name an identifier the criteria name are kept too, in run order. The
244
+ // cap keeps a broad identifier that matches every line from storing the whole run.
245
+ const named = identifiers.length === 0
246
+ ? []
247
+ : [...new Set(middle.filter((line) => identifiers.some((id) => line.includes(id))))]
248
+ .slice(0, NAMED_DETAIL_LINES_MAX)
249
+ .map((line) => (line.length > 500 ? `${line.slice(0, 500)}…` : line));
250
+ const dropped = middle.length - named.length;
241
251
  return [
242
252
  ...lines.slice(0, head),
243
253
  `… ${dropped} line${dropped === 1 ? "" : "s"} omitted …`,
254
+ ...named,
244
255
  ...lines.slice(lines.length - tail),
245
256
  ];
246
257
  }
258
+ const NAMED_DETAIL_LINES_MAX = 40;
259
+ /**
260
+ * The test and symbol names the acceptance criteria name: every backticked string, and every
261
+ * snake_case, camelCase, dotted or `::` token of at least 8 characters. An ordinary English word has
262
+ * none of `_`, `::`, `.` or an inner capital, so it never counts.
263
+ */
264
+ function criterionIdentifiers(acceptance) {
265
+ const found = new Set();
266
+ for (const criterion of acceptance) {
267
+ for (const match of criterion.matchAll(/`([^`]+)`/g)) {
268
+ if (match[1].trim())
269
+ found.add(match[1].trim());
270
+ }
271
+ for (const [token] of criterion.matchAll(/[A-Za-z_][A-Za-z0-9_]*(?:(?:::|\.)[A-Za-z_][A-Za-z0-9_]*)*/g)) {
272
+ if (token.length >= 8 && /_|::|\.|[a-z0-9][A-Z]/.test(token))
273
+ found.add(token);
274
+ }
275
+ }
276
+ return [...found];
277
+ }
247
278
  /**
248
279
  * Shorten a raw diagnostic message while keeping the end.
249
280
  *
@@ -263,6 +294,14 @@ export function boundedTail(text, maxChars) {
263
294
  return text.slice(text.length - maxChars);
264
295
  return `${text.slice(0, head)}${marker}${text.slice(text.length - tail)}`;
265
296
  }
297
+ /**
298
+ * The lines of a run in the order the command wrote them. A run without the combined output (a
299
+ * test's fake runner) reads as stdout followed by stderr.
300
+ */
301
+ function outputLines(result) {
302
+ const split = (text) => (text.trim() ? text.trim().split("\n") : []);
303
+ return result.output === undefined ? [...split(result.stdout), ...split(result.stderr)] : split(result.output);
304
+ }
266
305
  /**
267
306
  * A failure detail that keeps the part which explains the failure.
268
307
  *
@@ -275,10 +314,13 @@ export function boundedTail(text, maxChars) {
275
314
  * So: the command line for orientation, a few opening lines, then the **tail**, and the exit code
276
315
  * always. What was dropped is stated rather than silently vanishing, because a reader must be able
277
316
  * to tell a short failure from a truncated one.
317
+ *
318
+ * The lines are in the order the command wrote them. Joining all stdout before all stderr made the
319
+ * tail of a failed `cargo test` its stderr `Running …` lines and elided the panic on stdout.
278
320
  */
279
- export function boundedFailureDetail(command, stdoutLines, stderrLines, code, maxChars = FAILURE_DETAIL_MAX_CHARS) {
321
+ export function boundedFailureDetail(command, lines, code, maxChars = FAILURE_DETAIL_MAX_CHARS) {
280
322
  const cap = (line) => (line.length > 500 ? `${line.slice(0, 500)}…` : line);
281
- const body = [...stdoutLines, ...stderrLines].map(cap);
323
+ const body = lines.map(cap);
282
324
  const first = `$ ${command}`;
283
325
  const last = `exit ${code}`;
284
326
  const marker = (dropped) => `… ${dropped} line${dropped === 1 ? "" : "s"} omitted …`;
@@ -322,20 +364,36 @@ export function boundedFailureDetail(command, stdoutLines, stderrLines, code, ma
322
364
  *
323
365
  * A gate the timeout stopped did not exit: `timedOut` says so, and stderr ends with the limit it
324
366
  * reached. Read as exit 1, a gate that hung on the untouched base was a red base on the author.
367
+ *
368
+ * `output` is stdout and stderr together, in the order the lines arrived: a failure detail reads its
369
+ * tail from it.
325
370
  */
326
371
  export async function runBoundedVerificationCommand(command, workspace, timeoutMs = VERIFICATION_TIMEOUT_MS) {
327
372
  const argv = argvForBoundedCommand(command);
328
373
  const bin = argv[0];
329
374
  if (!bin)
330
375
  throw new Error("Verification command is empty");
376
+ // Chunks are not whole lines. Each stream keeps its partial last line until the line ends, so a
377
+ // stderr write between two stdout chunks cannot split a stdout line.
378
+ const lines = [];
379
+ const partial = { stdout: "", stderr: "" };
380
+ const collect = (stream) => (chunk) => {
381
+ const parts = `${partial[stream]}${String(chunk)}`.split("\n");
382
+ partial[stream] = parts.pop() ?? "";
383
+ lines.push(...parts);
384
+ };
385
+ const output = () => [...lines, ...[partial.stdout, partial.stderr].filter(Boolean)].join("\n");
331
386
  try {
332
- const { stdout, stderr } = await execFileAsync(bin, argv.slice(1), {
387
+ const pending = execFileAsync(bin, argv.slice(1), {
333
388
  cwd: workspace,
334
389
  timeout: timeoutMs,
335
390
  maxBuffer: VERIFICATION_MAX_BUFFER_BYTES,
336
391
  env: process.env,
337
392
  });
338
- return { stdout: String(stdout), stderr: String(stderr), code: 0 };
393
+ pending.child.stdout?.on("data", collect("stdout"));
394
+ pending.child.stderr?.on("data", collect("stderr"));
395
+ const { stdout, stderr } = await pending;
396
+ return { stdout: String(stdout), stderr: String(stderr), output: output(), code: 0 };
339
397
  }
340
398
  catch (error) {
341
399
  const err = error;
@@ -347,9 +405,11 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
347
405
  if (typeof err.code === "string")
348
406
  throw error;
349
407
  if (err.killed === true) {
408
+ const stopped = `Bridge stopped the gate: it did not exit within ${Math.round(timeoutMs / 1_000)} seconds.`;
350
409
  return {
351
410
  stdout: String(err.stdout ?? ""),
352
- stderr: `${String(err.stderr ?? "")}\nBridge stopped the gate: it did not exit within ${Math.round(timeoutMs / 1_000)} seconds.`.trimStart(),
411
+ stderr: `${String(err.stderr ?? "")}\n${stopped}`.trimStart(),
412
+ output: `${output()}\n${stopped}`.trimStart(),
353
413
  code: 1,
354
414
  timedOut: true,
355
415
  };
@@ -358,6 +418,7 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
358
418
  return {
359
419
  stdout: String(err.stdout ?? ""),
360
420
  stderr,
421
+ output: output() || stderr,
361
422
  code: typeof err.code === "number" ? err.code : 1,
362
423
  };
363
424
  }
@@ -536,12 +597,12 @@ export async function ensureTestEvidence(input) {
536
597
  // and would otherwise be witnessed as a passing test run that tested nothing.
537
598
  const ranNothing = detectGateRanNothing(command, result.stdout, result.stderr, result.code);
538
599
  if (ranNothing) {
539
- const detail = boundedFailureDetail(command, stdoutLines, stderrLines, result.code);
600
+ const detail = boundedFailureDetail(command, outputLines(result), result.code);
540
601
  throw new VerificationGateRanNothingError(command, result.code, ranNothing, detail);
541
602
  }
542
603
  if (result.code !== 0) {
543
604
  // Prefix must match FINALIZE_CONTRACT_PATTERN ("Agent report") — not Bridge-fault laundering.
544
- const detail = boundedFailureDetail(command, stdoutLines, stderrLines, result.code);
605
+ const detail = boundedFailureDetail(command, outputLines(result), result.code);
545
606
  // One line an operator can grep when a stored failure looks head-truncated. Reading the shape
546
607
  // back out of the database could not distinguish "the fix did not run" from "something
547
608
  // downstream re-truncated it"; this says which, at the moment it is produced.
@@ -563,11 +624,10 @@ export async function ensureTestEvidence(input) {
563
624
  }
564
625
  const rawDetails = [
565
626
  `$ ${command}`,
566
- ...stdoutLines,
567
- ...stderrLines,
627
+ ...outputLines(result),
568
628
  `exit ${result.code}`,
569
629
  ].map((line) => line.slice(0, 4_000));
570
- const details = boundedDetailLines(rawDetails, 100);
630
+ const details = boundedDetailLines(rawDetails, 100, criterionIdentifiers(acceptance));
571
631
  if (details.join("\n").trim().length < TEST_EVIDENCE_DETAILS_MIN) {
572
632
  throw new Error(`Agent report: Verification produced insufficient output for test evidence (${command})`);
573
633
  }
package/dist/execution.js CHANGED
@@ -526,7 +526,9 @@ export function buildDiagnosticPrompt(input) {
526
526
  ...(decisions.length ? ["", `OWNER DECISIONS\n${decisions.map((item) => `- ${item.question}: ${item.decision}`).join("\n")}`] : []),
527
527
  ...(input.verificationCommands.length ? ["", `BOUNDED VERIFICATION COMMANDS\n${input.verificationCommands.map((item) => `- ${item}`).join("\n")}`] : []),
528
528
  ...(gate ? ["", `GATE — Bridge re-runs \`${gate}\` after the next run; the agent cannot change or replace it.`
529
- + " If the fix is not in the agent's hands, answer confident:false and name the gate in root_cause."] : []),
529
+ + " A fix that needs a file outside the APPROVED CHANGE SCOPE is still a grounded diagnosis."
530
+ + " Answer confident:true and write the full repository path of each such file in fix_direction. Conductor makes it a scope decision."
531
+ + " Answer confident:false only when no file change can fix the cause (the gate itself, the environment), and name the gate in root_cause."] : []),
530
532
  "",
531
533
  "WITNESSED FAILURE OUTPUT",
532
534
  redactSecrets(input.failureOutput),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.147",
3
+ "version": "0.16.149",
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": {