@miraland-labs/conduit-bridge 0.16.161 → 0.16.171

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/driver.js CHANGED
@@ -291,82 +291,58 @@ export function buildAssignmentPrompt(context) {
291
291
  "- With it, write unverified_detail: one sentence naming exactly what was missing, in the owner's terms. \"Checking this needs the app deployed and running, and this task cannot deploy\" — not \"environment unavailable\".",
292
292
  // The owner reads outcome, limitations and unverified_detail on a phone. Dense prose was the
293
293
  // complaint on every delivery this trial; ASD-STE100's rules are the shortest fix.
294
- "- Write outcome, risks, limitations and every unverified_detail in Simplified Technical English (ASD-STE100): short sentences of at most 20 words, one fact or instruction per sentence, active voice, present tense, and the product's own terms (work package, delivery, gate, Hold) with no synonyms.", "", "When the work is finished, end your reply with exactly one fenced ```json block:", agentReportTemplate);
294
+ "- Write outcome, risks, limitations and every unverified_detail in Simplified Technical English (ASD-STE100): short sentences of at most 20 words, one fact or instruction per sentence, active voice, present tense, and the product's own terms (work package, delivery, gate, Hold) with no synonyms.", "", "- Delivery report limits: each details and acceptance_criteria entry is at most 4000 characters, with at most 100 entries per list; digest, name, and unverified_detail are non-empty and at most 500 characters; uri and pull_request_url are absolute URLs of at most 4000 characters.", "When the work is finished, end your reply with exactly one fenced ```json block:", agentReportTemplate);
295
295
  return lines.join("\n");
296
296
  }
297
297
  /**
298
- * Extract report JSON text from an agent reply, preferring the last closed ```json fence, then
299
- * earlier fences, then an unclosed one (truncated replies), then a balanced top-level object.
300
- *
301
- * A candidate only wins if it parses. The closed-fence pattern is lazy, so it ends at the first
302
- * ``` it meets — including one inside the report's own strings. That is not a rare accident: a
303
- * report about markdown rendering describes fences, so its summary contains them, and the extractor
304
- * then preferred a truncated fragment over the perfectly recoverable whole. Every retry wrote the
305
- * same summary and failed the same way, which is what "malformed report JSON, again" was.
298
+ * The reply's last ```json fenced block, or the whole reply when it has no fence. An opener ends its
299
+ * line (the cursor lane writes a sentence and opens the fence at its end), a closer stands alone,
300
+ * and a JSON string holds no line break, so a fence quoted inside the report's own strings never
301
+ * opens or closes one. Callers read the result with `JSON.parse` and nothing else.
306
302
  */
307
303
  export function extractAgentReportJsonText(text) {
308
- const candidates = [];
309
- // Anchor on each fence *opener*, last first — the report is the closing act of a reply that may
310
- // quote examples before it. Taking everything after an opener rather than up to the next ```
311
- // leaves the closing fence for the balanced-object scan to step over, which is what lets a report
312
- // containing fences, and a reply truncated mid-fence, both recover.
313
- for (const opener of [...text.matchAll(/```json[ \t]*\r?\n?/gi)].reverse()) {
314
- const start = (opener.index ?? 0) + opener[0].length;
315
- const value = text.slice(start).trim();
316
- if (value)
317
- candidates.push(value);
304
+ const lines = text.split(/\r?\n/);
305
+ let opener = -1;
306
+ for (let i = lines.length - 1; i >= 0 && opener < 0; i--)
307
+ if (/```json[ \t]*$/i.test(lines[i]))
308
+ opener = i;
309
+ if (opener < 0)
310
+ return text.trim();
311
+ const closer = lines.findIndex((line, i) => i > opener && /^[ \t]*```[ \t]*$/.test(line));
312
+ return lines.slice(opener + 1, closer < 0 ? undefined : closer).join("\n").trim();
313
+ }
314
+ /**
315
+ * A reply whose object does not parse or does not match its strict schema. Each issue is one zod
316
+ * path and message, so the re-ask and the failure detail name the field, not a sentence about it.
317
+ */
318
+ export class ReplyInvalid extends Error {
319
+ kind;
320
+ issues;
321
+ constructor(kind, issues) {
322
+ super(`${kind}: ${issues.join("; ")}`);
323
+ this.kind = kind;
324
+ this.issues = issues;
318
325
  }
319
- const start = text.indexOf("{");
320
- if (start >= 0)
321
- candidates.push(text.slice(start).trim());
322
- if (candidates.length === 0)
323
- throw new Error("Agent did not emit the required structured report");
324
- for (const candidate of candidates) {
326
+ }
327
+ /** Parse a reply's object and validate it strictly; anything else is a ReplyInvalid. */
328
+ export function parseReplyObject(kind, schema, text, structuredOutput) {
329
+ let raw = structuredOutput;
330
+ if (structuredOutput === undefined) {
325
331
  try {
326
- parseJsonObjectCandidate(candidate);
327
- return candidate;
332
+ raw = JSON.parse(extractAgentReportJsonText(text ?? ""));
328
333
  }
329
- catch {
330
- // Truncated at a fence inside a string, or prose that only looked like an object.
331
- }
332
- }
333
- // Nothing parsed: hand back the preferred candidate so the caller reports on the real report.
334
- return candidates[0];
335
- }
336
- /** Parse JSON, or the first balanced `{...}` object when the candidate is truncated prose. */
337
- export function parseJsonObjectCandidate(candidate) {
338
- try {
339
- return JSON.parse(candidate);
340
- }
341
- catch {
342
- const start = candidate.indexOf("{");
343
- if (start < 0)
344
- throw new Error("Agent emitted malformed report JSON");
345
- let depth = 0;
346
- let inString = false;
347
- for (let i = start; i < candidate.length; i++) {
348
- const ch = candidate[i];
349
- if (inString) {
350
- if (ch === "\\")
351
- i++;
352
- else if (ch === '"')
353
- inString = false;
354
- }
355
- else if (ch === '"')
356
- inString = true;
357
- else if (ch === "{")
358
- depth++;
359
- else if (ch === "}" && --depth === 0) {
360
- try {
361
- return JSON.parse(candidate.slice(start, i + 1));
362
- }
363
- catch {
364
- throw new Error("Agent emitted malformed report JSON");
365
- }
366
- }
334
+ catch (error) {
335
+ const reason = error instanceof Error ? error.message : "not JSON";
336
+ throw new ReplyInvalid(kind, [`(root): the last \`\`\`json fenced block, or the whole reply when it has none, is not JSON (${reason})`]);
367
337
  }
368
- throw new Error("Agent emitted malformed report JSON");
369
338
  }
339
+ const parsed = schema.safeParse(raw);
340
+ if (parsed.success)
341
+ return parsed.data;
342
+ throw new ReplyInvalid(kind, parsed.error.issues.slice(0, 20).map((issue) => {
343
+ const at = issue.path.reduce((path, key) => typeof key === "number" ? `${path}[${key}]` : path ? `${path}.${key}` : key, "");
344
+ return `${at || "(root)"}: ${issue.message}`;
345
+ }));
370
346
  }
371
347
  /**
372
348
  * Why this run could not decide an acceptance criterion.
@@ -474,37 +450,41 @@ function quoteCriterion(value) {
474
450
  const text = value.trim().replace(/\s+/g, " ");
475
451
  return `"${text.length > 120 ? `${text.slice(0, 117)}...` : text}"`;
476
452
  }
477
- /** Parse the agent's final fenced JSON block into a bounded report. */
478
- export function parseAgentReport(text, acceptance) {
479
- let raw;
480
- try {
481
- raw = parseJsonObjectCandidate(extractAgentReportJsonText(text));
482
- }
483
- catch (error) {
484
- if (error instanceof Error && /structured report|malformed report JSON/.test(error.message))
485
- throw error;
486
- throw new Error("Agent emitted malformed report JSON");
487
- }
488
- const stringList = z.array(z.string().trim().min(1).max(4_000)).max(100);
489
- sanitizeReportShape(raw);
490
- const parsed = z.object({
491
- outcome: z.string().trim().min(1).max(20_000),
492
- changes: stringList,
493
- verification: stringList,
494
- acceptance_results: z.array(z.object({ criterion: z.string().trim().min(1).max(4_000), status: z.enum(["met", "not_met", "unknown"]), unverified_reason: z.enum(UNVERIFIED_REASONS).optional(), unverified_detail: z.string().trim().min(1).max(500).optional() })).max(100),
495
- evidence: z.array(z.object({
496
- kind: z.enum(evidenceKinds), name: z.string().trim().min(1).max(500), uri: z.string().url().max(4_000).optional(),
497
- digest: z.string().trim().min(1).max(500).optional(), details: stringList, acceptance_criteria: stringList,
498
- })).max(100),
499
- assumptions: stringList, risks: stringList, limitations: stringList,
500
- head_commit: z.string().regex(/^[0-9a-f]{7,64}$/i).optional(),
501
- pull_request_url: z.string().url().max(4_000).optional(),
502
- }).strict().safeParse(raw);
503
- if (!parsed.success) {
504
- const issue = parsed.error.issues[0];
505
- const at = issue?.path?.length ? ` at ${issue.path.join(".")}` : "";
506
- throw new Error(`Agent report is invalid${at}: ${issue?.message ?? "unknown validation error"}`);
507
- }
453
+ const reportStringList = z.array(z.string().trim().min(1).max(4_000)).max(100);
454
+ export const agentReportSchema = z.object({
455
+ outcome: z.string().trim().min(1).max(20_000),
456
+ changes: reportStringList,
457
+ verification: reportStringList,
458
+ acceptance_results: z.array(z.object({
459
+ criterion: z.string().trim().min(1).max(4_000), status: z.enum(["met", "not_met", "unknown"]),
460
+ unverified_reason: z.enum(UNVERIFIED_REASONS).optional(), unverified_detail: z.string().trim().min(1).max(500).optional(),
461
+ })).max(100),
462
+ evidence: z.array(z.object({
463
+ kind: z.enum(evidenceKinds), name: z.string().trim().min(1).max(500), uri: z.string().url().max(4_000).optional(),
464
+ digest: z.string().trim().min(1).max(500).optional(), details: reportStringList, acceptance_criteria: reportStringList,
465
+ })).max(100),
466
+ assumptions: reportStringList, risks: reportStringList, limitations: reportStringList,
467
+ head_commit: z.string().regex(/^[0-9a-f]{7,64}$/i).optional(),
468
+ pull_request_url: z.string().url().max(4_000).optional(),
469
+ }).strict();
470
+ /** Strict generation requires every key; explicit null carries an optional field's absence. */
471
+ export const agentReportOutputSchema = agentReportSchema.extend({
472
+ acceptance_results: z.array(agentReportSchema.shape.acceptance_results.element.extend({
473
+ unverified_reason: z.enum(UNVERIFIED_REASONS).nullable().transform((value) => value ?? undefined),
474
+ unverified_detail: z.string().trim().min(1).max(500).nullable().transform((value) => value ?? undefined),
475
+ }).strict()).max(100),
476
+ evidence: z.array(agentReportSchema.shape.evidence.element.extend({
477
+ uri: z.string().url().max(4_000).nullable().transform((value) => value ?? undefined),
478
+ digest: z.string().trim().min(1).max(500).nullable().transform((value) => value ?? undefined),
479
+ }).strict()).max(100),
480
+ head_commit: z.string().regex(/^[0-9a-f]{7,64}$/i).nullable().transform((value) => value ?? undefined),
481
+ pull_request_url: z.string().url().max(4_000).nullable().transform((value) => value ?? undefined),
482
+ });
483
+ /** Parse the agent's final fenced JSON block or its native structured output into a bounded report. */
484
+ export function parseAgentReport(text, acceptance, structuredOutput) {
485
+ const data = structuredOutput === undefined
486
+ ? parseReplyObject("report_invalid", agentReportSchema, text)
487
+ : parseReplyObject("report_invalid", agentReportOutputSchema, text, structuredOutput);
508
488
  // Map every criterion reference — in the results and in the evidence — back to the approved text
509
489
  // before anything else reads it. Downstream (the packet, the reviewer, Today) sees text only; the
510
490
  // key is transport. An unknown key is refused here, because a key the agent invented cannot be
@@ -518,8 +498,8 @@ export function parseAgentReport(text, acceptance) {
518
498
  unknownKeys.push(value.trim());
519
499
  return value;
520
500
  };
521
- const acceptanceResults = parsed.data.acceptance_results.map((item) => ({ ...item, criterion: toApprovedText(item.criterion) }));
522
- const evidence = parsed.data.evidence.map((item) => ({ ...item, acceptance_criteria: item.acceptance_criteria.map(toApprovedText) }));
501
+ const acceptanceResults = data.acceptance_results.map((item) => ({ ...item, criterion: toApprovedText(item.criterion) }));
502
+ const evidence = data.evidence.map((item) => ({ ...item, acceptance_criteria: item.acceptance_criteria.map(toApprovedText) }));
523
503
  if (unknownKeys.length) {
524
504
  const valid = acceptance.length
525
505
  ? acceptance.map((criterion, index) => `${criterionKeyLabel(index)} = ${quoteCriterion(criterion)}`).join("; ")
@@ -550,7 +530,7 @@ export function parseAgentReport(text, acceptance) {
550
530
  // completed work instead of throwing the whole implementation away.
551
531
  const mappedCriteria = new Set(evidence.flatMap((item) => item.acceptance_criteria.map(criterionKey)));
552
532
  return {
553
- ...parsed.data,
533
+ ...data,
554
534
  evidence,
555
535
  acceptance_results: acceptanceResults.map((item) => {
556
536
  const { unverified_reason: reported, unverified_detail: detail, ...rest } = item;
@@ -588,86 +568,6 @@ export function normalizeEvidenceDigest(digest) {
588
568
  return `sha256:${trimmed.toLowerCase()}`;
589
569
  return trimmed;
590
570
  }
591
- /**
592
- * Tolerate benign agent-output noise before strict validation: drop empty/whitespace-only entries from
593
- * string arrays and remove empty optional scalars, so a single stray "" does not fail an otherwise-valid
594
- * delivery. Required non-empty scalars (outcome, evidence.name, criterion) stay strict — they cannot be
595
- * fabricated. Mutates `raw` in place.
596
- */
597
- function sanitizeReportShape(raw) {
598
- if (!raw || typeof raw !== "object")
599
- return;
600
- const obj = raw;
601
- const cleanList = (value) => Array.isArray(value)
602
- ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
603
- : value;
604
- for (const key of ["changes", "verification", "assumptions", "risks", "limitations"]) {
605
- if (key in obj)
606
- obj[key] = cleanList(obj[key]);
607
- }
608
- if (Array.isArray(obj.evidence)) {
609
- for (const item of obj.evidence) {
610
- if (!item || typeof item !== "object")
611
- continue;
612
- const evidence = item;
613
- if ("details" in evidence)
614
- evidence.details = cleanList(evidence.details);
615
- // Experimental lanes have repeatedly omitted this list on otherwise-useful test evidence.
616
- // An empty mapping asserts nothing; later logic downgrades any now-unsupported "met" result.
617
- if (!("acceptance_criteria" in evidence) || evidence.acceptance_criteria == null)
618
- evidence.acceptance_criteria = [];
619
- else
620
- evidence.acceptance_criteria = cleanList(evidence.acceptance_criteria);
621
- // Agents emit null/"" for optional fields they have no value for — treat as absent.
622
- if ("digest" in evidence && (typeof evidence.digest !== "string" || evidence.digest.trim() === ""))
623
- delete evidence.digest;
624
- else if (typeof evidence.digest === "string")
625
- evidence.digest = normalizeEvidenceDigest(evidence.digest);
626
- // Drop non-URL / empty uris so an optional bad uri doesn't fail the whole report.
627
- if ("uri" in evidence && (typeof evidence.uri !== "string" || !z.string().url().max(4_000).safeParse(evidence.uri).success))
628
- delete evidence.uri;
629
- // Witness is Bridge-authored provenance. An agent claiming it must not survive parse.
630
- if ("witness" in evidence)
631
- delete evidence.witness;
632
- }
633
- }
634
- // Same rule for optional top-level scalars: null/"" means absent, not invalid.
635
- for (const key of ["head_commit", "pull_request_url"]) {
636
- if (key in obj && (typeof obj[key] !== "string" || obj[key].trim() === ""))
637
- delete obj[key];
638
- }
639
- // And the same rule for keys Conduit does not model at all. An unknown key carrying a real value
640
- // still fails validation below — it may be the agent asserting something outside the contract, and
641
- // silently dropping that would hide it. An empty one is only noise, and discarding an otherwise
642
- // valid delivery over it cost a full staging canary on 2026-07-22 ("assumptions_note": null).
643
- for (const key of Object.keys(obj)) {
644
- if (!reportKeys.has(key) && isSemanticallyEmpty(obj[key]))
645
- delete obj[key];
646
- }
647
- }
648
- /** Top-level keys the delivery contract models. Keep in step with the schema in parseAgentReport. */
649
- const reportKeys = new Set([
650
- "outcome", "changes", "verification", "acceptance_results", "evidence",
651
- "assumptions", "risks", "limitations", "head_commit", "pull_request_url",
652
- ]);
653
- /**
654
- * Words agents write when a field has no value. Observed live: after `"assumptions_note": null` was
655
- * handled, the very next real run emitted `"assumptions_note": "none"` and failed the same way.
656
- * These carry no claim, so an unknown key holding one is noise rather than an assertion.
657
- */
658
- const noValueWords = new Set(["none", "n/a", "na", "nil", "null", "nothing", "not applicable", "-", "—"]);
659
- /** Absent-in-practice: null, undefined, blank/no-value string, or an empty array/object. `false` and `0` are values. */
660
- function isSemanticallyEmpty(value) {
661
- if (value === null || value === undefined)
662
- return true;
663
- if (typeof value === "string")
664
- return value.trim() === "" || noValueWords.has(value.trim().toLowerCase());
665
- if (Array.isArray(value))
666
- return value.length === 0;
667
- if (typeof value === "object")
668
- return Object.keys(value).length === 0;
669
- return false;
670
- }
671
571
  /** A count the vendor printed, or undefined when the envelope holds no usable number. */
672
572
  function usageNumber(value) {
673
573
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
@@ -745,6 +645,8 @@ export const claudeCodeDriver = {
745
645
  // The prompt goes on stdin (`claude -p` reads it there when no prompt argument is given): Linux
746
646
  // caps one argument at 128 KiB (MAX_ARG_STRLEN) and an assignment contract exceeds that.
747
647
  const args = ["-p", "--output-format", "json", "--max-turns", String(maxTurns)];
648
+ if (input.outputSchemaPath)
649
+ args.push("--json-schema", await readFile(input.outputSchemaPath, "utf8"));
748
650
  args.push(...modelArgs(claudeCodeDriver, input.model));
749
651
  if (input.resumeSessionId)
750
652
  args.push("--resume", input.resumeSessionId);
@@ -777,7 +679,10 @@ export const claudeCodeDriver = {
777
679
  if (code !== 0 || !message || message.is_error) {
778
680
  return { status: "failed", resultText: message?.result ?? null, sessionId, usage, signal: agentExitSignal({ exit_code: code, stderr, stdout, model: input.model }), error: boundedTail(message?.result || stderr || `agent exited with code ${code}`, 20_000) };
779
681
  }
780
- return { status: "completed", resultText: message.result ?? "", sessionId, usage };
682
+ return {
683
+ status: "completed", resultText: message.result ?? "", sessionId, usage,
684
+ ...(input.outputSchemaPath ? { structuredOutput: message.structured_output ?? null } : {}),
685
+ };
781
686
  },
782
687
  };
783
688
  /**
@@ -803,6 +708,8 @@ export function codexExecArgs(input, sandbox) {
803
708
  ? ["exec", "resume", "--json", "-c", `sandbox_mode="${sandbox}"`]
804
709
  : ["exec", "--json", "--sandbox", sandbox];
805
710
  args.push(...modelArgs(codexDriver, input.model));
711
+ if (input.outputSchemaPath)
712
+ args.push("--output-schema", input.outputSchemaPath);
806
713
  const networkAccess = input.networkAccess
807
714
  ?? (sandbox === "workspace-write" && input.capabilities?.includes("external_network"));
808
715
  if (sandbox === "workspace-write" && networkAccess) {
@@ -916,7 +823,17 @@ export const codexDriver = {
916
823
  if (code !== 0) {
917
824
  return { status: "failed", resultText, sessionId: parsed.sessionId, usage: parsed.usage, signal: agentExitSignal({ exit_code: code, stderr, stdout, model: input.model }), error: boundedTail(stderr || stdout || `codex exited with code ${code}`, 20_000) };
918
825
  }
919
- return { status: "completed", resultText, sessionId: parsed.sessionId, usage: parsed.usage };
826
+ let structuredOutput = null;
827
+ if (input.outputSchemaPath && parsed.resultText !== null) {
828
+ try {
829
+ structuredOutput = JSON.parse(parsed.resultText);
830
+ }
831
+ catch { /* Validation refuses null. */ }
832
+ }
833
+ return {
834
+ status: "completed", resultText, sessionId: parsed.sessionId, usage: parsed.usage,
835
+ ...(input.outputSchemaPath ? { structuredOutput } : {}),
836
+ };
920
837
  },
921
838
  };
922
839
  /**
@@ -2055,8 +1972,10 @@ export function boundedEnvironment(fuel, fuelSource = "conduit", extraEnv) {
2055
1972
  "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "no_proxy", "SSL_CERT_FILE", "NODE_EXTRA_CA_CERTS",
2056
1973
  // The machine's Rust toolchain and build cache. Without CARGO_TARGET_DIR an agent compiled every
2057
1974
  // dependency cold in its worktree, so no `cargo test` it started finished (acme-sim SV1b, 2026-10-10);
2058
- // CARGO_PROFILE_* keeps its build identical to the gate's, so the gate reuses what the agent built.
2059
- "CARGO_HOME", "RUSTUP_HOME", "CARGO_TARGET_DIR",
1975
+ // CARGO_PROFILE_* and CARGO_INCREMENTAL keep its build identical to the gate's, so the gate reuses
1976
+ // what the agent built; an incremental agent build next to a non-incremental gate build in one
1977
+ // target dir left mixed objects that failed to link (acme-sim, 2026-10-15 and 2026-10-18).
1978
+ "CARGO_HOME", "RUSTUP_HOME", "CARGO_TARGET_DIR", "CARGO_INCREMENTAL",
2060
1979
  ...Object.keys(process.env).filter((name) => name.startsWith("CARGO_PROFILE_")),
2061
1980
  ...(fuelSource === "local" ? LOCAL_VENDOR_ENV : [])];
2062
1981
  const env = Object.fromEntries(allowed.flatMap((name) => process.env[name] === undefined ? [] : [[name, process.env[name]]]));
@@ -240,20 +240,33 @@ export function boundedDetailLines(lines, maxLines, identifiers = []) {
240
240
  // is proved by one line in the middle of a long suite, and a reviewer must not infer it from the
241
241
  // summary. So the lines that name an identifier the criteria name are kept too, in run order. The
242
242
  // cap keeps a broad identifier that matches every line from storing the whole run.
243
+ // A run of several suites prints one summary per suite, and only the last one sits in the tail:
244
+ // cargo's "test result: ok. 614 passed" for the cli binary says nothing about the core binary. A
245
+ // criterion settled by the totals (a test count delta) needs every summary, so they are kept too.
246
+ const summaries = middle.filter((line) => SUITE_SUMMARY.some((form) => form.test(line))).slice(0, NAMED_DETAIL_LINES_MAX);
243
247
  const named = identifiers.length === 0
244
248
  ? []
245
- : [...new Set(middle.filter((line) => identifiers.some((id) => line.includes(id))))]
249
+ : [...new Set(middle.filter((line) => !summaries.includes(line) && identifiers.some((id) => line.includes(id))))]
246
250
  .slice(0, NAMED_DETAIL_LINES_MAX)
247
251
  .map((line) => (line.length > 500 ? `${line.slice(0, 500)}…` : line));
248
- const dropped = middle.length - named.length;
252
+ const dropped = middle.length - summaries.length - named.length;
249
253
  return [
250
254
  ...lines.slice(0, head),
251
255
  `… ${dropped} line${dropped === 1 ? "" : "s"} omitted …`,
256
+ ...summaries,
252
257
  ...named,
253
258
  ...lines.slice(lines.length - tail),
254
259
  ];
255
260
  }
256
261
  const NAMED_DETAIL_LINES_MAX = 40;
262
+ /** One suite's totals line: cargo, vitest, unittest, pytest, go test. */
263
+ const SUITE_SUMMARY = [
264
+ /^test result: /,
265
+ /^\s*(Test Files|Tests)\s+\d/,
266
+ /^Ran \d+ tests?\b/,
267
+ /^=+ .*\b(passed|failed)\b.* in [\d.]+s =+$/,
268
+ /^(ok|FAIL)\s+\S+\s+[\d.]+s$/,
269
+ ];
257
270
  /**
258
271
  * The test and symbol names the acceptance criteria name: every backticked string, and every
259
272
  * snake_case, camelCase, dotted or `::` token of at least 8 characters. An ordinary English word has