@molecule/api-ai-tools 1.13.1 → 1.14.1

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/utilities.js CHANGED
@@ -536,9 +536,25 @@ const PRINTING_REPORTERS = new Set(['list', 'line', 'dot', 'basic', 'spec']);
536
536
  * edited `app/src` while the run was still walking — which voids the run.
537
537
  */
538
538
  export const PLAYWRIGHT_FAILURES_INLINE_ENV = 'PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE';
539
+ /**
540
+ * A step that stores the status of the step before it in a variable: `__proof_slow=$?`.
541
+ *
542
+ * The variable name is the shell's own (`[A-Za-z_][A-Za-z0-9_]*`) and the value is EXACTLY the
543
+ * special parameter, bare or double-quoted, so nothing else can be running here. A capture whose
544
+ * value is anything else — `x=$(rebuild)`, `x=$?; rm -rf …` (a separate statement anyway) — is a
545
+ * program, not a status, and is not one of these.
546
+ */
547
+ const STATUS_CAPTURE_STEP = /^[A-Za-z_][A-Za-z0-9_]*=(?:\$\?|"\$\?"|\$\{\?\})$/;
548
+ /**
549
+ * The arithmetic body `exit $((…))` may carry and still run nothing: variable names, numbers and
550
+ * arithmetic operators only. No `$`, backtick, `;` or comma is allowed, so no command substitution
551
+ * and no expansion of a variable into something that runs can hide in it.
552
+ */
553
+ const STATUS_EXIT_ARITHMETIC = /^[A-Za-z0-9_|&+\-*/%^<>=!~() \t]*$/;
539
554
  /**
540
555
  * Whether a command step only reports or discards the status of the steps before it: `echo EXIT
541
- * $?`, `echo $?`, `printf …`, `true`, `exit 0`.
556
+ * $?`, `echo $?`, `printf …`, `true`, `exit 0`, a status capture (`__proof_slow=$?`), or an
557
+ * `exit $((a | b))` over statuses already captured.
542
558
  *
543
559
  * The shape models and the platform's own re-run notes send routinely ends on one of these —
544
560
  * `npx playwright test … > /tmp/pw.log 2>&1; echo EXIT $?` — and reading the split's last part as
@@ -548,12 +564,33 @@ export const PLAYWRIGHT_FAILURES_INLINE_ENV = 'PLAYWRIGHT_LIST_PRINT_FAILURES_IN
548
564
  * punctuation are not part of the step; {@link streamingBackgroundCommand} strips those before
549
565
  * asking, and strips them here too for the split it is given.
550
566
  *
567
+ * A status CAPTURE is the same kind of step, and the platform's own chained proof walks end on a
568
+ * run of them: `… <browser run>; __proof_slow=$?; echo __PROOF_EXIT__ …; exit $((__proof_fast |
569
+ * __proof_slow))` (`chainedProofCommand`, molecule-dev `post-deploy-proof-run.ts`). Reading the
570
+ * browser half as a status step is what lets the run keep its reporter, its machine report and the
571
+ * inline-failure switch — without it, every platform proof ran its browser suite on the request's
572
+ * own `line` reporter, which writes no ✓/✘ mark and no per-failure error into the log the wait
573
+ * reads (xr-molecule-flash-x03 +27:44).
574
+ *
575
+ * Only the two shapes whose whole value is the shell's own status are read this way; anything that
576
+ * could run — a command substitution, a backticked word, a suffix after the status — stays a step.
577
+ *
551
578
  * @param step - One statement of the command, as the `&&`/`||`/`;` split left it.
552
579
  * @returns True when the step runs nothing but a status report.
553
580
  */
554
581
  function isStatusOnlyStep(step) {
555
- const text = step.replace(/[\s)}]+$/, '').trim();
556
- return /^(?:echo|printf)\b/.test(text) || /^true$/.test(text) || /^exit(?:\s+\S+)?$/.test(text);
582
+ const text = step.trim();
583
+ // An enclosing group's closer is punctuation, not part of the step — but the `))` that closes an
584
+ // arithmetic expansion is part of the statement, so that shape is read off the text as written.
585
+ const withoutCloser = text.replace(/[\s)}]+$/, '');
586
+ if (/^(?:echo|printf)\b/.test(withoutCloser) || /^true$/.test(withoutCloser))
587
+ return true;
588
+ if (/^exit(?:\s+\S+)?$/.test(withoutCloser))
589
+ return true;
590
+ if (STATUS_CAPTURE_STEP.test(withoutCloser))
591
+ return true;
592
+ const arithmetic = /^exit\s+\$\(\((.*)\)\)$/.exec(text);
593
+ return arithmetic !== null && STATUS_EXIT_ARITHMETIC.test(arithmetic[1] ?? '');
557
594
  }
558
595
  /**
559
596
  * Rewrite a command about to run in the background so its log grows while it
@@ -565,13 +602,15 @@ function isStatusOnlyStep(step) {
565
602
  * It is dropped: `wait_for_task` already returns the end of the log.
566
603
  * - any output-only filter piped after a SUITE runner (`| grep … | tail -20`), which hides the
567
604
  * failing test's own error and leaves the pipeline's exit status — the filter's — as the run's.
568
- * Every such stage is dropped ({@link stripTrailingSuiteFilters}); `wait_for_task` already
569
- * returns the end of the full log, and a run whose tally shows a failure must never report 0
570
- * (xr-molecule-flash-m101 +25:54).
605
+ * Every such stage is dropped from EVERY suite run in the command, not only the last
606
+ * ({@link stripTrailingSuiteFilters}); `wait_for_task` already returns the end of the full log,
607
+ * and a run whose tally shows a failure must never report 0 (xr-molecule-flash-m101 +25:54).
571
608
  * - `playwright test` with no `--reporter`, which runs the config's reporter —
572
609
  * often `html`, which prints nothing per test. `--reporter=list` is added
573
610
  * when the playwright run is the command's last step, piped or not: it writes each result as
574
- * it finishes, and it is the shape the platform reads a background suite back in.
611
+ * it finishes, and it is the shape the platform reads a background suite back in. Every EARLIER
612
+ * Playwright invocation gets the same reporter, so its log is readable while it walks; the
613
+ * machine `json` report stays on the last invocation.
575
614
  *
576
615
  * The run ends up with exactly ONE reporter that prints each failure — `list`. A run that names
577
616
  * any other printing reporter has it REPLACED, never joined: two of them write every test result
@@ -634,20 +673,22 @@ export function streamingBackgroundCommand(command, context, reportPath) {
634
673
  // A filter after the suite runner is not output the platform can read: it hides the failing
635
674
  // test's own error, and its exit status replaces the run's with the filter's — m101's
636
675
  // `… | grep -E "✘|passed|failed|skipped" | tail -20` reported `exitCode: 0` for a run with a
637
- // failure in it. Every stage the parse proves is output-only is dropped; the end of the FULL
638
- // log comes back anyway.
639
- const filtered = stripTrailingSuiteFilters(out);
640
- if (filtered.dropped) {
676
+ // failure in it. Every stage the parse proves is output-only is dropped from EVERY run, earlier
677
+ // walks included; the end of the FULL log comes back anyway.
678
+ const filtered = suiteFilterDrops(out);
679
+ if (filtered.drops.length > 0) {
641
680
  out = filtered.command;
642
- // A lone trailing `| tail -N` keeps its own sentence: the reason it goes is that it holds all
643
- // output back until the command ends, not that a filter cannot be read back.
644
- const tailOnly = TRAILING_TAIL.exec(filtered.dropped);
645
- changes.push(tailOnly && tailOnly[0].trim() === filtered.dropped
646
- ? `Dropped the trailing \`${filtered.dropped}\`: it holds all output back until the command ends, ` +
647
- 'and wait_for_task returns the end of the log anyway.'
648
- : `Dropped \`${filtered.dropped}\` from after the test runner: a filtered log cannot be ` +
649
- 'diagnosed — the failing test’s own error is not in it — and wait_for_task returns the ' +
650
- 'end of the full log anyway.');
681
+ for (const dropped of filtered.drops) {
682
+ // A lone trailing `| tail -N` keeps its own sentence: the reason it goes is that it holds all
683
+ // output back until the command ends, not that a filter cannot be read back.
684
+ const tailOnly = TRAILING_TAIL.exec(dropped);
685
+ changes.push(tailOnly && tailOnly[0].trim() === dropped
686
+ ? `Dropped the trailing \`${dropped}\`: it holds all output back until the command ends, ` +
687
+ 'and wait_for_task returns the end of the log anyway.'
688
+ : `Dropped \`${dropped}\` from after the test runner: a filtered log cannot be ` +
689
+ 'diagnosed — the failing test’s own error is not in it — and wait_for_task returns the ' +
690
+ 'end of the full log anyway.');
691
+ }
651
692
  }
652
693
  const tail = TRAILING_TAIL.exec(out);
653
694
  if (tail) {
@@ -863,8 +904,116 @@ export function streamingBackgroundCommand(command, context, reportPath) {
863
904
  if (addToRun('--retries=0'))
864
905
  changes.push(`Added \`--retries=0\`: ${why}.`);
865
906
  }
907
+ // Every EARLIER Playwright invocation writes the same log, so it is put on the progressive
908
+ // reporter too — not only the invocation this launcher measures (see
909
+ // {@link earlierPlaywrightReporters}).
910
+ const earlier = earlierPlaywrightReporters(out);
911
+ if (earlier.changes.length > 0) {
912
+ out = earlier.command;
913
+ changes.push(...earlier.changes);
914
+ }
866
915
  return { command: out, changes, ...(report === null ? {} : { reportPath: report }) };
867
916
  }
917
+ /**
918
+ * Put every Playwright invocation BEFORE the command's last one on the progressive `list`
919
+ * reporter.
920
+ *
921
+ * A compound command's earlier walk is not a preview: it writes the same log the executor reads,
922
+ * and on X1 xr-molecule-flash-l21 (+21:27) the first 3.6-minute walk kept its own
923
+ * `--reporter=line`, which prints `[n/N] <title>` rows with no ✓/✘ mark and no per-failure error.
924
+ * What came back was `72 passed` and two failure NAMES; the error bodies existed only in the walk's
925
+ * discarded log, and diagnosing them needed another run.
926
+ *
927
+ * Only the reporter SET is touched, and only printing reporters in it ({@link PRINTING_REPORTERS})
928
+ * are replaced by ONE `list` — the same rule the last invocation gets. A run that names no
929
+ * reporter has the config reporter, which may print nothing until the end, so `--reporter=list` is
930
+ * added where the parse can place it. Every other flag (`--grep`, `--project`, `--config`,
931
+ * `--workers`, `--timeout`, selections, exclusions, retries) and every non-printing reporter
932
+ * (`json`, `html`, `junit`, or one this build does not know) is left exactly as written.
933
+ *
934
+ * The LAST invocation is deliberately not this function's: {@link streamingBackgroundCommand}
935
+ * already decides its reporters, and it alone owns the machine `json` report, which must never be
936
+ * claimed for an earlier walk. A statement the parse cannot place (inside a nested `sh -c` string)
937
+ * is left as written — there is no offset in this command to splice at.
938
+ *
939
+ * @param command - The command after every other rewrite.
940
+ * @returns The rewritten command, and one sentence per reporter change made.
941
+ */
942
+ function earlierPlaywrightReporters(command) {
943
+ const located = locatedSegments(command);
944
+ const runs = [];
945
+ for (let i = 0; i < located.length; i += 1) {
946
+ const segment = located[i]?.segment;
947
+ if (segment && SUITE_RUN_COMMANDS.some((re) => re.test(segment.text)))
948
+ runs.push(i);
949
+ }
950
+ const rewrites = [];
951
+ let out = command;
952
+ // Right to left, so each site's offsets still describe the text they were measured on.
953
+ for (const at of runs.slice(0, -1).reverse()) {
954
+ const site = located[at];
955
+ if (!site || site.start === null || site.end === null)
956
+ continue;
957
+ const raw = out.slice(site.start, site.end);
958
+ // A statement that only lists or prints tests is not a run (`--list`, `--help`).
959
+ if (!isPlaywrightRunCommand(raw))
960
+ continue;
961
+ const reporters = lastReporterFlag(raw);
962
+ if (reporters === null) {
963
+ const insertAt = statementArgumentEnd(out, site.start, site.end);
964
+ if (insertAt === null)
965
+ continue;
966
+ out = `${out.slice(0, insertAt)} --reporter=list${out.slice(insertAt)}`;
967
+ rewrites.push({ kind: 'added', from: '' });
968
+ continue;
969
+ }
970
+ const printing = reporters.values.filter((value) => PRINTING_REPORTERS.has(value));
971
+ // A set with no printing reporter is the executor's own artifact (`json`, `html`, `junit`, or
972
+ // a reporter this build does not know) and is left exactly as it was.
973
+ if (printing.length === 0)
974
+ continue;
975
+ if (printing.length === 1 && printing[0] === 'list')
976
+ continue;
977
+ let placed = false;
978
+ const kept = reporters.values
979
+ .map((value) => {
980
+ if (!PRINTING_REPORTERS.has(value))
981
+ return value;
982
+ if (placed)
983
+ return null;
984
+ placed = true;
985
+ return 'list';
986
+ })
987
+ .filter((value) => value !== null);
988
+ out =
989
+ out.slice(0, site.start + reporters.start) +
990
+ `--reporter${reporters.separator}${kept.join(',')}` +
991
+ out.slice(site.start + reporters.end);
992
+ rewrites.push({
993
+ kind: printing.includes('list') ? 'dropped' : 'switched',
994
+ from: reporters.values.join(','),
995
+ });
996
+ }
997
+ const changes = [];
998
+ const count = (kind) => rewrites.filter((rewrite) => rewrite.kind === kind).length;
999
+ if (count('added') > 0)
1000
+ changes.push(`Added \`--reporter=list\` to ${count('added') === 1 ? 'the earlier test run' : 'each earlier test run'} ` +
1001
+ 'so each test result is written to the log as it finishes (a config reporter may print ' +
1002
+ 'nothing until the end).');
1003
+ const switched = rewrites.find((rewrite) => rewrite.kind === 'switched');
1004
+ if (switched)
1005
+ changes.push(`Switched ${count('switched') === 1 ? 'the earlier test run’s' : 'each earlier test run’s'} ` +
1006
+ `reporters to \`--reporter=list\`: \`list\` writes each test result as it finishes and is ` +
1007
+ 'the shape the platform reads back, and it is the only printing reporter — two of them ' +
1008
+ 'write every result into the log twice (X1 flash-m83).');
1009
+ const dropped = rewrites.find((rewrite) => rewrite.kind === 'dropped');
1010
+ if (dropped)
1011
+ changes.push(`Dropped a second printing reporter from ` +
1012
+ `${count('dropped') === 1 ? 'the earlier test run’s' : 'each earlier test run’s'} ` +
1013
+ `\`--reporter=${dropped.from}\`: two reporters that both print each failure write every ` +
1014
+ 'test result into the log twice, so this run keeps `list` alone.');
1015
+ return { command: out, changes };
1016
+ }
868
1017
  /**
869
1018
  * Put Playwright's standard `json` report on a FOREGROUND run that already names its own reporters.
870
1019
  *
@@ -1306,50 +1455,48 @@ function lastSuitePipeline(command) {
1306
1455
  * @returns The located segments and the last suite stage's index among them, or null.
1307
1456
  */
1308
1457
  function lastSuiteRun(command) {
1458
+ return suiteRunSites(command).at(-1) ?? null;
1459
+ }
1460
+ /**
1461
+ * Every suite-run statement of a command, in command order, each with the parse's segments.
1462
+ *
1463
+ * A command may start the suite more than once — a whole-directory walk followed by a single
1464
+ * spec, or a Hurl run followed by a browser walk — and every one of them writes the log the
1465
+ * executor reads. Reading only the last one left the earlier walk's failing test's own error
1466
+ * behind a `| tail -5` while the LAST invocation was rewritten normally
1467
+ * (xr-molecule-flash-l21 +21:27: a 3.6-minute walk returned `72 passed` with no error body when
1468
+ * the driver ran `… specs 2>&1 | tail -5; echo START; … auth.spec.ts`).
1469
+ *
1470
+ * A command that runs no test at all is not a set of runs ({@link commandRunsNoTests}); nor is a
1471
+ * statement that only lists tests, which the reporter pass asks about per statement
1472
+ * ({@link isPlaywrightRunCommand}).
1473
+ *
1474
+ * @param command - The shell command.
1475
+ * @returns One entry per suite-running statement, all sharing the parse's segments.
1476
+ */
1477
+ function suiteRunSites(command) {
1309
1478
  if (commandRunsNoTests(command))
1310
- return null;
1479
+ return [];
1311
1480
  const located = locatedSegments(command);
1312
- let at = -1;
1481
+ const sites = [];
1313
1482
  for (let i = 0; i < located.length; i += 1) {
1314
1483
  const segment = located[i]?.segment;
1315
1484
  if (segment && SUITE_RUN_COMMANDS.some((re) => re.test(segment.text)))
1316
- at = i;
1485
+ sites.push({ located, at: i });
1317
1486
  }
1318
- return at < 0 ? null : { located, at };
1487
+ return sites;
1319
1488
  }
1320
1489
  /**
1321
- * Drop the output-only filters piped after the command's suite runner.
1322
- *
1323
- * A filter after the runner is not "the end of the log" the executor asked for: it hides the
1324
- * failing test's own error (it usually matches none of `✘|passed|failed`), and the pipeline's exit
1325
- * status becomes the FILTER's, so `… 2>&1 | grep -E "✘|passed|failed|skipped" | tail -20` came back
1326
- * `exitCode: 0` for a run with a failure in it (xr-molecule-flash-m101 +25:54). The tool returns
1327
- * the end of the full log anyway, so the filter only costs diagnosability.
1328
- *
1329
- * Only a contiguous suffix of the stages after the runner is cut, and only stages the parse proves
1330
- * are output-only ({@link isOutputOnlyFilter}): a `| tee` or `| xargs` keeps its place, and a
1331
- * caller that reports the run's exit code must then treat it as unknown
1332
- * ({@link suiteRunPipedAfterRunner}).
1333
- *
1334
- * A statement after the run's own pipeline (`; echo done`, `&& echo ok`) does not protect the
1335
- * filters before it: the filter is thrown away and the trailing statement is kept, so the log the
1336
- * run writes still holds every failure (xr-molecule-flash-m121 — a `| tail -5; echo done` kept its
1337
- * tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
1338
- * that command back).
1339
- *
1340
- * The command around the dropped stages is left as written: a group they sit in keeps its closer
1341
- * (`( … | tail -25)` → `( … )`, never `( …` — xr-molecule-flash-m418 +23:31), and a stage the cut
1342
- * would leave a closer dangling inside is not dropped at all.
1490
+ * The output-only filters piped after ONE suite runner, as a slice to remove — or null when this
1491
+ * runner has none this reader can prove output-only.
1343
1492
  *
1344
1493
  * @param command - The shell command.
1345
- * @returns The rewritten command and the stages dropped.
1494
+ * @param located - The parse's located segments ({@link locatedSegments}).
1495
+ * @param at - The runner's index among them.
1496
+ * @returns The slice to drop, or null.
1346
1497
  */
1347
- export function stripTrailingSuiteFilters(command) {
1348
- const run = lastSuiteRun(command);
1349
- if (run === null)
1350
- return { command, dropped: '' };
1351
- const { located, at } = run;
1352
- // Where the suite's own pipeline ends: the first stage after the runner that is not joined to
1498
+ function suiteFilterCut(command, located, at) {
1499
+ // Where this suite's own pipeline ends: the first stage after the runner that is not joined to
1353
1500
  // its predecessor by a single `|`. Everything from there on is another statement, kept as it is.
1354
1501
  let end = at;
1355
1502
  for (let i = at + 1; i < located.length; i += 1) {
@@ -1365,15 +1512,15 @@ export function stripTrailingSuiteFilters(command) {
1365
1512
  first = i;
1366
1513
  }
1367
1514
  if (first === end + 1)
1368
- return { command, dropped: '' };
1515
+ return null;
1369
1516
  const previous = located[first - 1];
1370
1517
  const stage = located[first];
1371
1518
  const lastDropped = located[end];
1372
1519
  if (!previous || !stage || !lastDropped)
1373
- return { command, dropped: '' };
1520
+ return null;
1374
1521
  // A stage inside a nested `sh -c` string has no offsets in this command: nothing can be cut.
1375
1522
  if (previous.end === null || stage.start === null || lastDropped.end === null)
1376
- return { command, dropped: '' };
1523
+ return null;
1377
1524
  // Every stage dropped must be provably whole inside the command's own structure. A group
1378
1525
  // closer attached to one of them closes a group the pipeline below still runs in
1379
1526
  // (`( … | tail -5) | cat`), and a stage that OPENS a group it does not close (`| { cat; }`) has
@@ -1383,17 +1530,17 @@ export function stripTrailingSuiteFilters(command) {
1383
1530
  for (let i = first; i <= end; i += 1) {
1384
1531
  const droppedStage = located[i];
1385
1532
  if (!droppedStage || droppedStage.end === null)
1386
- return { command, dropped: '' };
1533
+ return null;
1387
1534
  const boundary = statementBoundary(command, droppedStage.start, droppedStage.end);
1388
1535
  if (boundary === null || boundary.leavesGroupOpen)
1389
- return { command, dropped: '' };
1536
+ return null;
1390
1537
  if (i < end && boundary.textEnd !== droppedStage.end)
1391
- return { command, dropped: '' };
1538
+ return null;
1392
1539
  boundaries.push(boundary);
1393
1540
  }
1394
1541
  const lastBoundary = boundaries[boundaries.length - 1];
1395
1542
  if (lastBoundary === undefined)
1396
- return { command, dropped: '' };
1543
+ return null;
1397
1544
  // Where the dropped stages' own text ends. The tokenizer attaches the closer of an enclosing
1398
1545
  // group to the stage's last word (`tail -25)`), so cutting at {@link LocatedSegment.end} deleted
1399
1546
  // the `)` and left `( … 2>&1` — a command no shell parses, which the background runner rejected
@@ -1401,14 +1548,71 @@ export function stripTrailingSuiteFilters(command) {
1401
1548
  const dropEnd = lastBoundary.textEnd;
1402
1549
  const pipe = stage.between.indexOf('|');
1403
1550
  if (pipe < 0)
1404
- return { command, dropped: '' };
1551
+ return null;
1405
1552
  const cut = previous.end + pipe;
1406
- // The stages dropped are a middle slice when a statement follows, so the text after them is
1407
- // kept — `; echo done` still runs, and so does the punctuation closing the group they sat in.
1408
- return {
1409
- command: `${command.slice(0, cut).trimEnd()}${command.slice(dropEnd)}`,
1410
- dropped: command.slice(cut, dropEnd).trim(),
1411
- };
1553
+ return { cut, dropEnd, dropped: command.slice(cut, dropEnd).trim() };
1554
+ }
1555
+ /**
1556
+ * Drop the output-only filters piped after EVERY suite runner in the command.
1557
+ *
1558
+ * A filter after a runner is not "the end of the log" the executor asked for: it hides the
1559
+ * failing test's own error (it usually matches none of `✘|passed|failed`), and the pipeline's exit
1560
+ * status becomes the FILTER's, so `… 2>&1 | grep -E "✘|passed|failed|skipped" | tail -20` came back
1561
+ * `exitCode: 0` for a run with a failure in it (xr-molecule-flash-m101 +25:54). The tool returns
1562
+ * the end of the full log anyway, so the filter only costs diagnosability.
1563
+ *
1564
+ * EVERY run is stripped, not only the command's last: a compound command's earlier walk writes
1565
+ * the same log, and its `| tail -5` threw away the error body of a 3.6-minute Playwright walk
1566
+ * whose two failure names were all the executor got back (xr-molecule-flash-l21 +21:27). Only a
1567
+ * run the parse places is touched, and only its own pipeline — a later statement's filters belong
1568
+ * to it.
1569
+ *
1570
+ * Only a contiguous suffix of the stages after the runner is cut, and only stages the parse proves
1571
+ * are output-only ({@link isOutputOnlyFilter}): a `| tee` or `| xargs` keeps its place, and a
1572
+ * caller that reports the run's exit code must then treat it as unknown
1573
+ * ({@link suiteRunPipedAfterRunner}).
1574
+ *
1575
+ * A statement after the run's own pipeline (`; echo done`, `&& echo ok`) does not protect the
1576
+ * filters before it: the filter is thrown away and the trailing statement is kept, so the log the
1577
+ * run writes still holds every failure (xr-molecule-flash-m121 — a `| tail -5; echo done` kept its
1578
+ * tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
1579
+ * that command back).
1580
+ *
1581
+ * The command around the dropped stages is left as written: a group they sit in keeps its closer
1582
+ * (`( … | tail -25)` → `( … )`, never `( …` — xr-molecule-flash-m418 +23:31), and a stage the cut
1583
+ * would leave a closer dangling inside is not dropped at all.
1584
+ *
1585
+ * @param command - The shell command.
1586
+ * @returns The rewritten command and the stages dropped, one entry per suite run, in command order.
1587
+ */
1588
+ function suiteFilterDrops(command) {
1589
+ const cuts = [];
1590
+ for (const site of suiteRunSites(command)) {
1591
+ const cut = suiteFilterCut(command, site.located, site.at);
1592
+ if (cut !== null)
1593
+ cuts.push(cut);
1594
+ }
1595
+ if (cuts.length === 0)
1596
+ return { command, drops: [] };
1597
+ // Right to left, so every cut's offsets still describe the text it was measured on.
1598
+ let out = command;
1599
+ for (const cut of [...cuts].sort((a, b) => b.cut - a.cut)) {
1600
+ // The stages dropped are a middle slice when a statement follows, so the text after them is
1601
+ // kept — `; echo done` still runs, and so does the punctuation closing the group they sat in.
1602
+ out = `${out.slice(0, cut.cut).trimEnd()}${out.slice(cut.dropEnd)}`;
1603
+ }
1604
+ return { command: out, drops: cuts.map((cut) => cut.dropped) };
1605
+ }
1606
+ /**
1607
+ * Drop the output-only filters piped after the command's suite runners
1608
+ * ({@link suiteFilterDrops}).
1609
+ *
1610
+ * @param command - The shell command.
1611
+ * @returns The rewritten command, and the stages dropped in command order (`''` when none were).
1612
+ */
1613
+ export function stripTrailingSuiteFilters(command) {
1614
+ const { command: out, drops } = suiteFilterDrops(command);
1615
+ return { command: out, dropped: drops.join(' ') };
1412
1616
  }
1413
1617
  /**
1414
1618
  * Where the suite runner's exit code comes from: the runner itself, or a stage piped after it.
@@ -2423,6 +2627,43 @@ export function runningCauseCommand(log, own) {
2423
2627
  const read = `cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null`;
2424
2628
  return `${read} | tail -c ${REPEATING_CAUSE_TAIL_BYTES}`;
2425
2629
  }
2630
+ /** Bytes of a running log a diagnostic note reads back: one {@link REPEATING_CAUSE_TAIL_BYTES} window. */
2631
+ export const RUNNING_LOG_READ_BYTES = REPEATING_CAUSE_TAIL_BYTES;
2632
+ /**
2633
+ * The shell command whose stdout is a bounded CONTIGUOUS read of a running task's own log: the
2634
+ * log's byte size, then its last {@link RUNNING_LOG_READ_BYTES} bytes.
2635
+ *
2636
+ * {@link runningOutputSliceCommand} is the wrong read for a diagnosis, and l07 is the finding: it
2637
+ * stitches the first 40 grep-selected failure lines to the last 1 000 bytes with a blank line
2638
+ * between, so the tail is DISJOINT from the headers above it. Read as one log, the last numbered
2639
+ * header absorbs whatever the tail starts with — a clipped skipped-test title became the cause of
2640
+ * a genuine failure (xr-molecule-flash-l07). A contiguous window can only ever hold real
2641
+ * neighbouring lines of one log.
2642
+ *
2643
+ * The size rides along so a caller can say when the window is only the log's end
2644
+ * ({@link parseRunningLogRead}); without it a tail reads as the whole run.
2645
+ *
2646
+ * @param log - The task log path.
2647
+ * @param own - A file the command redirects its own output to, if any.
2648
+ * @returns The shell command, safe to hand to the execution backend as-is.
2649
+ */
2650
+ export function runningLogReadCommand(log, own) {
2651
+ const read = `cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null`;
2652
+ return `{ ${read} | wc -c; ${read} | tail -c ${RUNNING_LOG_READ_BYTES}; }`;
2653
+ }
2654
+ /**
2655
+ * The two parts of {@link runningLogReadCommand}'s answer.
2656
+ *
2657
+ * @param stdout - The command's stdout.
2658
+ * @returns The window and the whole log's byte size, or null when the size line is absent (the
2659
+ * read did not run, so nothing may be said about the log).
2660
+ */
2661
+ export function parseRunningLogRead(stdout) {
2662
+ const sized = /^\s*(\d+)\s*\n/.exec(stdout);
2663
+ if (sized === null)
2664
+ return null;
2665
+ return { text: stdout.slice(sized[0].length), bytes: Number(sized[1]) };
2666
+ }
2426
2667
  /**
2427
2668
  * DISTINCT tests that must share one signature before a running suite's repeating cause is named.
2428
2669
  * A test the log prints twice, or a test that failed twice, is still one test.
@@ -2513,6 +2754,12 @@ function failureBody(block) {
2513
2754
  .map((l) => l.replace(ANSI_SGR, '').trim())
2514
2755
  .filter((l) => l &&
2515
2756
  !LIST_RESULT_LINE.test(l) &&
2757
+ // A SKIPPED test's own row (` - 41 [chromium] › user-fetch-errors.spec.ts:88:3 › …`) is
2758
+ // a progress line, never a failure's body: a block with no body of its own is followed by
2759
+ // whatever the reporter printed next, and that row became the failure's "cause" when the
2760
+ // wait's stitched display was read as one log (xr-molecule-flash-l07). A `- waiting for`
2761
+ // call-log line stays — it names the locator, and the dash carries no index.
2762
+ !/^\s*-\s+\d+\s+/.test(l) &&
2516
2763
  !/^(?:at\s|\d+ \||>\s*\d+ \||\|)/.test(l) &&
2517
2764
  !/^Error Context:/.test(l));
2518
2765
  }
@@ -3870,14 +4117,14 @@ regressedSpecs = []) {
3870
4117
  'handler shape, a header or CORS.');
3871
4118
  }
3872
4119
  // A text that is the TAIL of a longer log says so in its first line (`…(N earlier characters
3873
- // omitted)`, the shape `background-tasks` writes when a log is capped): what it holds is not
4120
+ // omitted)`, or bytes for a byte-bounded read): what it holds is not
3874
4121
  // the run's list of failures, only the part that survived the cut. Saying "N failures in this
3875
4122
  // run" over it reads as complete, and did: xr-molecule-flash-m123's collected 42 KB diagnosis
3876
4123
  // log was clipped to its last 8 KB, and this note grouped 2 of the run's 14 failures under
3877
4124
  // "fix every cause below" while the 11 that shared the run's one real cause were gone
3878
4125
  // (2026-10-17). The root fix is to hand this function the whole log; this says so when a
3879
4126
  // caller cannot.
3880
- const clipped = /^…\((\d+) earlier characters omitted\)/.exec(log);
4127
+ const clipped = /^…\((\d+) earlier (characters|bytes) omitted\)/.exec(log);
3881
4128
  // The missing-header reading leads the list: four unrelated locators failing while every
3882
4129
  // snapshot shows the page painted and un-navigated is ONE fact about the route, and the list
3883
4130
  // below would otherwise read as four separate defects (xr-molecule-flash-m193 rewrote the
@@ -3887,7 +4134,7 @@ regressedSpecs = []) {
3887
4134
  const header = (missingHeader === null ? '' : `${missingHeader}\n`) +
3888
4135
  (clipped !== null
3889
4136
  ? `${total} failure${total === 1 ? '' : 's'} in the LAST part of this run’s log — its ` +
3890
- `first ${clipped[1]} characters were clipped, so a cause that failed earlier is ` +
4137
+ `first ${clipped[1]} ${clipped[2]} were clipped, so a cause that failed earlier is ` +
3891
4138
  `missing here. Fix every cause below${connections.length > 0 ? ' the app’s code can produce, and re-send the connection error once as it is' : ''}, ` +
3892
4139
  `and read the whole run log before concluding:\n`
3893
4140
  : `${total} failure${total === 1 ? '' : 's'} in this run, grouped by cause — fix every cause ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.13.1",
3
+ "version": "1.14.1",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",