@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.
- package/dist/ensure-test-evidence.js +73 -13
- package/dist/execution.js +3 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 ?? "")}\
|
|
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,
|
|
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,
|
|
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
|
-
...
|
|
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
|
-
+ "
|
|
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.
|
|
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": {
|