@molecule/api-ai-tools 1.6.0 → 1.6.2

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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-16T15:16:34.345Z
6
+ Generated: 2026-10-16T15:37:48.365Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -1040,6 +1040,26 @@ function pathArgError(path: unknown, tool: string): string | null
1040
1040
 
1041
1041
  **Returns:** An actionable error string, or null when `path` is a non-empty string.
1042
1042
 
1043
+ #### `emptyCallError(input, tool)`
1044
+
1045
+ The error for a file tool called with NO arguments at all — `edit_file {}`, the
1046
+ shape a truncated provider stream leaves behind.
1047
+
1048
+ The path message is the wrong reading of it: "requires a non-empty path
1049
+ argument" says the call was understood and the path was left out, so the model
1050
+ retries the call it just made instead of sending the whole call again (x0
1051
+ flash-m83 +29:32: `edit_file {}`, one round lost). An empty object and a
1052
+ missing `path` are different failures and deserve different sentences.
1053
+
1054
+ ```typescript
1055
+ function emptyCallError(input: unknown, tool: string): string | null
1056
+ ```
1057
+
1058
+ - `input` — The raw tool input.
1059
+ - `tool` — The tool name, for the message.
1060
+
1061
+ **Returns:** The truncation message, or null when the call carried any argument.
1062
+
1043
1063
  #### `directoryReadHint(message, path)`
1044
1064
 
1045
1065
  Detect a "read/edit targeted a directory, not a file" failure from a backend
@@ -1233,9 +1253,11 @@ The cause repeating among a running suite's failures, or null.
1233
1253
  Playwright's `list` reporter prints one line per test as it finishes — its mark, its title
1234
1254
  path and how long it ran — and defers each failure's error to the summary at the end, so
1235
1255
  mid-run the only signature those lines carry is the duration. Where the log carries the
1236
- failure bodies (a `line` reporter's output, or a finished run's summary) the repeated error
1237
- message itself is the signature, which is stronger (it needs no assumption about how long a
1238
- failure should take), so the message is looked for first.
1256
+ failure bodies (a `line` reporter's output, or a finished run's summary) the signature is the
1257
+ error itself — {@link failureMessage}: the error class plus the locator or the values that
1258
+ differ — which is stronger (it needs no assumption about how long a failure should take), so
1259
+ the message is looked for first and the duration is then left with the tests whose bodies the
1260
+ log does NOT carry.
1239
1261
 
1240
1262
  Either way the point is the same: a suite whose failures repeat one cause spends its whole
1241
1263
  runtime proving that one cause, and the executor watching it can be told so at the third
@@ -1290,9 +1312,11 @@ after another with no statement of which of them share a cause, so a run of 19 f
1290
1312
  bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a
1291
1313
  single cause, two of them found by runs stopped before they walked the rest.
1292
1314
 
1293
- The signature is the failures' own error line where the log carries their bodies (a finished
1294
- run, or a `line` reporter), and how long they took where it does not (a still-running `list`
1295
- log). A test the log printed twice, or failed twice, is one test.
1315
+ The signature is the cause itself where the log carries the failures' bodies (a finished run,
1316
+ or a `line` reporter) — {@link failureMessage}: the error class plus the locator or the values
1317
+ that differ — and how long they took where it does not (a still-running `list` log, which is
1318
+ why a duration group is labelled as a duration and not as a proven cause). A test the log
1319
+ printed twice, or failed twice, is one test.
1296
1320
 
1297
1321
  ```typescript
1298
1322
  function failuresByCause(log: string): FailureCause[]
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkL9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA0DnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAk4DxF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkL9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAoEnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAm5DxF"}
package/dist/tools.js CHANGED
@@ -161,12 +161,20 @@ function waitProgressNote(o) {
161
161
  const sentence = parts.join('; ');
162
162
  return ` ${sentence[0].toUpperCase()}${sentence.slice(1)}.`;
163
163
  }
164
- import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, failureCausesNote, findPatternArgs, fusedLine, htmlOutline, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isValidGlob, linesWithinSerialized, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, } from './utilities.js';
164
+ import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, emptyCallError, failureCausesNote, findPatternArgs, fusedLine, htmlOutline, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isValidGlob, linesWithinSerialized, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, } from './utilities.js';
165
165
  /**
166
166
  * How long exec_command waits for a command it backgrounded only because the
167
167
  * command asked for more than the tool's ceiling, before handing back a task id.
168
168
  */
169
169
  const OVER_CEILING_GRACE_MS = 45_000;
170
+ /**
171
+ * The share of its budget a command must use before the "you asked for more than
172
+ * this tool's ceiling" note is worth attaching. Below it the clamp changed nothing
173
+ * about the run, and the line read as a warning about work that had already
174
+ * finished (xr-molecule-flash-m83 +53:11: a Hurl run of 25 s against a 290 s
175
+ * ceiling carried the note).
176
+ */
177
+ const CEILING_NOTE_MIN_USED = 0.5;
170
178
  /**
171
179
  * The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
172
180
  * redirect below fires only when the read FAILS, so a model that knows the hoisted
@@ -733,6 +741,9 @@ export function buildTools(backend, config) {
733
741
  }
734
742
  },
735
743
  async read_file(input) {
744
+ const emptyErr = emptyCallError(input, 'read_file');
745
+ if (emptyErr)
746
+ return { error: emptyErr };
736
747
  // Several paths in ONE call. The executor issues one tool call per model
737
748
  // round-trip 89% of the time (X0: 464 assistant messages, 515 tool calls,
738
749
  // a ~10s mean gap), so a turn's wall clock IS its round-trip count — and
@@ -833,6 +844,9 @@ export function buildTools(backend, config) {
833
844
  return readOneFile(input.path, input);
834
845
  },
835
846
  async write_file(input) {
847
+ const emptyErr = emptyCallError(input, 'write_file');
848
+ if (emptyErr)
849
+ return { error: emptyErr };
836
850
  const argErr = pathArgError(input.path, 'write_file');
837
851
  if (argErr)
838
852
  return { error: argErr };
@@ -879,6 +893,9 @@ export function buildTools(backend, config) {
879
893
  }
880
894
  },
881
895
  async edit_file(input) {
896
+ const emptyErr = emptyCallError(input, 'edit_file');
897
+ if (emptyErr)
898
+ return { error: emptyErr };
882
899
  const argErr = pathArgError(input.path, 'edit_file');
883
900
  if (argErr)
884
901
  return { error: argErr };
@@ -1544,16 +1561,28 @@ export function buildTools(backend, config) {
1544
1561
  const effectiveBudgetMs = Number.isFinite(requestedMs) && requestedMs > 0 && commandBudgetMs
1545
1562
  ? Math.min(requestedMs, commandBudgetMs)
1546
1563
  : commandBudgetMs;
1547
- const ceilingNote = wantsMore && budgetSeconds
1548
- ? `You asked for ${Math.round(requestedMs / 1000)}s; this tool's ceiling is ${budgetSeconds}s. ` +
1549
- 'Split the work into smaller commands (one test file, one build step) rather than ' +
1550
- 're-running the same long one.'
1551
- : null;
1564
+ const startedAt = Date.now();
1552
1565
  const result = await backend.run(command, {
1553
1566
  cwd,
1554
1567
  timeout: execTimeoutMs,
1555
1568
  ...(effectiveBudgetMs ? { budgetMs: effectiveBudgetMs } : {}),
1556
1569
  });
1570
+ const elapsedMs = Date.now() - startedAt;
1571
+ // The clamped ask is only worth a line when the clamp was felt: a command
1572
+ // that asked for 600 s and finished in 25 s says nothing about splitting
1573
+ // work, and the note read as a warning about a run that had already
1574
+ // finished cleanly (xr-molecule-flash-m83 +53:11 — a Hurl run, 25 s).
1575
+ // Half the budget is the line; below it no smaller unit would have been
1576
+ // chosen any differently. A run that actually hit the ceiling is answered
1577
+ // by the exit-124 message below, not by this note.
1578
+ const ceilingNote = wantsMore &&
1579
+ budgetSeconds &&
1580
+ effectiveBudgetMs &&
1581
+ elapsedMs >= effectiveBudgetMs * CEILING_NOTE_MIN_USED
1582
+ ? `You asked for ${Math.round(requestedMs / 1000)}s; this tool's ceiling is ${budgetSeconds}s. ` +
1583
+ 'Split the work into smaller commands (one test file, one build step) rather than ' +
1584
+ 're-running the same long one.'
1585
+ : null;
1557
1586
  // truncateMiddle (not truncate): a failing build/test/migration puts its
1558
1587
  // error at the TAIL, so keep the head AND the tail — head-only truncation
1559
1588
  // strands the executor with passing progress and no failure reason.
@@ -286,6 +286,21 @@ export declare function findPatternArgs(pattern: string): string;
286
286
  * @returns An actionable error string, or null when `path` is a non-empty string.
287
287
  */
288
288
  export declare function pathArgError(path: unknown, tool: string): string | null;
289
+ /**
290
+ * The error for a file tool called with NO arguments at all — `edit_file {}`, the
291
+ * shape a truncated provider stream leaves behind.
292
+ *
293
+ * The path message is the wrong reading of it: "requires a non-empty path
294
+ * argument" says the call was understood and the path was left out, so the model
295
+ * retries the call it just made instead of sending the whole call again (x0
296
+ * flash-m83 +29:32: `edit_file {}`, one round lost). An empty object and a
297
+ * missing `path` are different failures and deserve different sentences.
298
+ *
299
+ * @param input - The raw tool input.
300
+ * @param tool - The tool name, for the message.
301
+ * @returns The truncation message, or null when the call carried any argument.
302
+ */
303
+ export declare function emptyCallError(input: unknown, tool: string): string | null;
289
304
  /**
290
305
  * Detect a "read/edit targeted a directory, not a file" failure from a backend
291
306
  * error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
@@ -573,9 +588,11 @@ export interface RepeatingFailure {
573
588
  * Playwright's `list` reporter prints one line per test as it finishes — its mark, its title
574
589
  * path and how long it ran — and defers each failure's error to the summary at the end, so
575
590
  * mid-run the only signature those lines carry is the duration. Where the log carries the
576
- * failure bodies (a `line` reporter's output, or a finished run's summary) the repeated error
577
- * message itself is the signature, which is stronger (it needs no assumption about how long a
578
- * failure should take), so the message is looked for first.
591
+ * failure bodies (a `line` reporter's output, or a finished run's summary) the signature is the
592
+ * error itself — {@link failureMessage}: the error class plus the locator or the values that
593
+ * differ — which is stronger (it needs no assumption about how long a failure should take), so
594
+ * the message is looked for first and the duration is then left with the tests whose bodies the
595
+ * log does NOT carry.
579
596
  *
580
597
  * Either way the point is the same: a suite whose failures repeat one cause spends its whole
581
598
  * runtime proving that one cause, and the executor watching it can be told so at the third
@@ -652,9 +669,11 @@ export interface FailureCause {
652
669
  * bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a
653
670
  * single cause, two of them found by runs stopped before they walked the rest.
654
671
  *
655
- * The signature is the failures' own error line where the log carries their bodies (a finished
656
- * run, or a `line` reporter), and how long they took where it does not (a still-running `list`
657
- * log). A test the log printed twice, or failed twice, is one test.
672
+ * The signature is the cause itself where the log carries the failures' bodies (a finished run,
673
+ * or a `line` reporter) — {@link failureMessage}: the error class plus the locator or the values
674
+ * that differ — and how long they took where it does not (a still-running `list` log, which is
675
+ * why a duration group is labelled as a duration and not as a proven cause). A test the log
676
+ * printed twice, or failed twice, is one test.
658
677
  *
659
678
  * @param log - The log of a finished or running run.
660
679
  * @returns The causes, the one with the most tests first.
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
package/dist/utilities.js CHANGED
@@ -730,6 +730,30 @@ export function pathArgError(path, tool) {
730
730
  return `${tool} requires a non-empty "path" argument (a file path relative to the project root, e.g. "api/src/handlers/index.ts").`;
731
731
  return null;
732
732
  }
733
+ /**
734
+ * The error for a file tool called with NO arguments at all — `edit_file {}`, the
735
+ * shape a truncated provider stream leaves behind.
736
+ *
737
+ * The path message is the wrong reading of it: "requires a non-empty path
738
+ * argument" says the call was understood and the path was left out, so the model
739
+ * retries the call it just made instead of sending the whole call again (x0
740
+ * flash-m83 +29:32: `edit_file {}`, one round lost). An empty object and a
741
+ * missing `path` are different failures and deserve different sentences.
742
+ *
743
+ * @param input - The raw tool input.
744
+ * @param tool - The tool name, for the message.
745
+ * @returns The truncation message, or null when the call carried any argument.
746
+ */
747
+ export function emptyCallError(input, tool) {
748
+ const empty = input === undefined ||
749
+ input === null ||
750
+ (typeof input === 'object' &&
751
+ !Array.isArray(input) &&
752
+ Object.keys(input).length === 0);
753
+ return empty
754
+ ? `${tool} arrived with no arguments at all — the call was cut short, usually a truncated response. Send the whole call again.`
755
+ : null;
756
+ }
733
757
  /**
734
758
  * Detect a "read/edit targeted a directory, not a file" failure from a backend
735
759
  * error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
@@ -1259,23 +1283,106 @@ function failedTests(log) {
1259
1283
  * while running "beforeEach" hook.`), which changes with the hook; the thrown error below it is
1260
1284
  * what repeats verbatim, so it is preferred.
1261
1285
  *
1262
- * @param block - One block from {@link failureBlocks}.
1286
+ * @param body - The block's lines, from {@link failureBody}.
1263
1287
  * @returns The error line, or null when the block has no body.
1264
1288
  */
1265
- function failureMessage(block) {
1266
- const body = block
1289
+ function failureErrorLine(body) {
1290
+ const thrown = body.find((l) => /^[A-Za-z_$][\w.$]*(?:Error|Exception)\b/.test(l));
1291
+ return thrown ?? body[0] ?? null;
1292
+ }
1293
+ /**
1294
+ * A failure block's own words, one per line: its body without the runner's progress lines (the
1295
+ * `list` reporter interleaves them with the `line` reporter's blocks), without a code frame or an
1296
+ * `at …` line (where the error happened, not what it said), and without the report's file list.
1297
+ *
1298
+ * @param block - One block from {@link failureBlocks}.
1299
+ * @returns The body's lines, ANSI stripped and trimmed.
1300
+ */
1301
+ function failureBody(block) {
1302
+ return block
1267
1303
  .split('\n')
1268
1304
  .slice(1)
1269
1305
  .map((l) => l.replace(ANSI_SGR, '').trim())
1270
- // Progress lines the `list` reporter interleaves with the `line` reporter's blocks are not
1271
- // the block's own words, and a code frame or an `at …` line is where the error happened, not
1272
- // what it said.
1273
1306
  .filter((l) => l &&
1274
1307
  !LIST_RESULT_LINE.test(l) &&
1275
1308
  !/^(?:at\s|\d+ \||>\s*\d+ \||\|)/.test(l) &&
1276
1309
  !/^Error Context:/.test(l));
1277
- const thrown = body.find((l) => /^[A-Za-z_$][\w.$]*(?:Error|Exception)\b/.test(l));
1278
- return thrown ?? body[0] ?? null;
1310
+ }
1311
+ /**
1312
+ * What a failure block says the failure is ABOUT, when its error line does not already say it:
1313
+ * the locator it waited for, the navigation it waited on, or the locator and the expected/received
1314
+ * pair of a failed assertion.
1315
+ *
1316
+ * Without this, every `TimeoutError: page.press: Timeout 5000ms exceeded.` in a log is one
1317
+ * message, because that line names the ACTION (`page.press`) and never its subject. X1 flash-m84
1318
+ * (2026-10-16) had four such failures in one run — one waiting on
1319
+ * `button:has-text("Publish Article")`, one on `.article-preview h1`, one on
1320
+ * `.article-preview').first().locator('.author')`, one on a generated `h1:has-text("Test
1321
+ * Article …")` — and the run's summary called them two causes while its mid-run note called them
1322
+ * one. The subject is what separates causes; it is in the block, and nothing else in the block
1323
+ * carries it.
1324
+ *
1325
+ * @param body - The block's lines, from {@link failureBody}.
1326
+ * @returns The detail line, or null when the block names none.
1327
+ */
1328
+ function failureDetail(body) {
1329
+ const waited = body.find((l) => /^-?\s*waiting for (?:locator\(|navigation|element|selector)/.test(l));
1330
+ if (waited !== undefined)
1331
+ return waited.replace(/^-\s*/, '');
1332
+ // A failed assertion names its locator and the two values that differ.
1333
+ const locator = body.find((l) => /^Locator:\s/.test(l));
1334
+ if (locator === undefined)
1335
+ return null;
1336
+ const parts = [
1337
+ locator,
1338
+ body.find((l) => /^Expected\b/.test(l)),
1339
+ body.find((l) => /^Received\b/.test(l)),
1340
+ ];
1341
+ return parts.filter((p) => p !== undefined).join(' · ');
1342
+ }
1343
+ /**
1344
+ * A cause signature with the values that differ between two tests of ONE cause replaced by `<n>`:
1345
+ * a duration, a generated id or timestamp, a host's port.
1346
+ *
1347
+ * The duration is the one that matters. A suite that sets `actionTimeout: 5_000` — the X1
1348
+ * realworld suite does — spends `5 s` plus teardown on every "the locator never appeared"
1349
+ * failure, so `~7 s` is a property of the CONFIG, not of the cause, and four different missing
1350
+ * elements print the same number (X1 flash-m84, 2026-10-16). Two tests waiting on the same
1351
+ * locator for 5 s and for 10 s wait for the same thing; two waiting on different locators do not
1352
+ * become one cause by timing out together.
1353
+ *
1354
+ * @param s - An error line, with its detail.
1355
+ * @returns The signature.
1356
+ */
1357
+ function normalizedSignature(s) {
1358
+ return (s
1359
+ // A duration: the suite's own timeout, in whatever unit the runner printed.
1360
+ .replace(/\b\d+(?:\.\d+)?\s*(?:ms|s|m)\b/g, '<n>')
1361
+ // A host's port. The shape is what makes it a port: a bare `:32` is a line number.
1362
+ .replace(/\b(\d{1,3}(?:\.\d{1,3}){3}|localhost|0\.0\.0\.0):\d+/g, '$1:<n>')
1363
+ // A generated id, a timestamp, a serial: five digits or more is no part of a selector.
1364
+ .replace(/\b\d{5,}\b/g, '<n>')
1365
+ .replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, '<id>'));
1366
+ }
1367
+ /**
1368
+ * What separates one cause from another in a `playwright test` log: the error CLASS plus the
1369
+ * normalized message — the error line, the thing it waited for or asserted on, and the values
1370
+ * with the test's own data normalized out.
1371
+ *
1372
+ * One test printed twice, or two tests failing the same way, share a signature; two tests whose
1373
+ * errors differ in their locator do not, however alike the rest of the line is.
1374
+ *
1375
+ * @param block - One block from {@link failureBlocks}.
1376
+ * @returns The signature, or null when the block has no body.
1377
+ */
1378
+ function failureMessage(block) {
1379
+ const body = failureBody(block);
1380
+ const line = failureErrorLine(body);
1381
+ if (line === null)
1382
+ return null;
1383
+ const detail = failureDetail(body);
1384
+ const signature = detail !== null && !line.includes(detail) ? `${line} — ${detail}` : line;
1385
+ return normalizedSignature(signature);
1279
1386
  }
1280
1387
  /**
1281
1388
  * The largest group of items sharing a key.
@@ -1325,9 +1432,11 @@ function distinctTests(tests) {
1325
1432
  * Playwright's `list` reporter prints one line per test as it finishes — its mark, its title
1326
1433
  * path and how long it ran — and defers each failure's error to the summary at the end, so
1327
1434
  * mid-run the only signature those lines carry is the duration. Where the log carries the
1328
- * failure bodies (a `line` reporter's output, or a finished run's summary) the repeated error
1329
- * message itself is the signature, which is stronger (it needs no assumption about how long a
1330
- * failure should take), so the message is looked for first.
1435
+ * failure bodies (a `line` reporter's output, or a finished run's summary) the signature is the
1436
+ * error itself — {@link failureMessage}: the error class plus the locator or the values that
1437
+ * differ — which is stronger (it needs no assumption about how long a failure should take), so
1438
+ * the message is looked for first and the duration is then left with the tests whose bodies the
1439
+ * log does NOT carry.
1331
1440
  *
1332
1441
  * Either way the point is the same: a suite whose failures repeat one cause spends its whole
1333
1442
  * runtime proving that one cause, and the executor watching it can be told so at the third
@@ -1349,12 +1458,14 @@ export function repeatingFailure(log) {
1349
1458
  // DISTINCT test, so the largest group is the cause the most tests share — not the one whose
1350
1459
  // failures happen to have been printed the most times.
1351
1460
  const byMessage = new Map();
1461
+ const withBody = new Set();
1352
1462
  for (const block of failureBlocks(text)) {
1463
+ const m = FAILURE_BLOCK_HEADER.exec(block.split('\n')[0] ?? '');
1464
+ const { where, title } = splitTitlePath(m?.[1] ?? '');
1353
1465
  const message = failureMessage(block);
1354
1466
  if (message === null)
1355
1467
  continue;
1356
- const m = FAILURE_BLOCK_HEADER.exec(block.split('\n')[0] ?? '');
1357
- const { where, title } = splitTitlePath(m?.[1] ?? '');
1468
+ withBody.add(testKey({ where, title, seconds: null }));
1358
1469
  const group = byMessage.get(message);
1359
1470
  if (group)
1360
1471
  group.push({ where, title, seconds: null });
@@ -1372,10 +1483,16 @@ export function repeatingFailure(log) {
1372
1483
  seconds: null,
1373
1484
  tests: messageGroup.members,
1374
1485
  };
1375
- // Otherwise the durations: the largest set of failures that took the same time to within the
1376
- // band. A test that runs to its timeout prints slightly different durations on each attempt
1377
- // (15.6 s, 15.1 s, 15.0 s), so the band is 0.5 s or 5% of the duration, whichever is larger.
1378
- const tests = distinctTests(failedTests(text));
1486
+ // Otherwise the durations — of the tests the log says nothing ELSE about. A test whose failure
1487
+ // body is in the log has a signature, and how long it took must not overrule it: a suite whose
1488
+ // config sets a timeout makes every failure that waits for anything land on the same number, so
1489
+ // the duration would merge causes the bodies just separated (X1 flash-m84, 2026-10-16 — four
1490
+ // failures at 6.8-7.4 s, four locators, `actionTimeout: 5_000`). What is left is the shape this
1491
+ // was written for: a `list`-only log, or a run whose bodies have not been printed yet, where
1492
+ // the duration is the ONLY signature the log carries. A test that runs to its timeout prints
1493
+ // slightly different durations on each attempt (15.6 s, 15.1 s, 15.0 s), so the band is 0.5 s
1494
+ // or 5% of the duration, whichever is larger.
1495
+ const tests = distinctTests(failedTests(text)).filter((t) => !withBody.has(testKey(t)));
1379
1496
  const withSeconds = tests.filter((t) => t.seconds !== null);
1380
1497
  let best = [];
1381
1498
  for (const t of withSeconds) {
@@ -1395,17 +1512,6 @@ export const REPEATING_FAILURE_LISTED = 5;
1395
1512
  export const FAILURE_CAUSES_LISTED = 5;
1396
1513
  /** How many of one cause's tests that summary names before it says "and N more". */
1397
1514
  export const FAILURE_CAUSE_TESTS_LISTED = 6;
1398
- /**
1399
- * The shortest unambiguous tail of a logged path (`…/specs/e2e/articles.spec.ts:32` →
1400
- * `e2e/articles.spec.ts:32`).
1401
- *
1402
- * @param where - The location the log wrote.
1403
- * @returns The last two path segments, with the line number.
1404
- */
1405
- function shortWhere(where) {
1406
- const parts = where.split('/');
1407
- return parts.slice(-2).join('/');
1408
- }
1409
1515
  /**
1410
1516
  * The shortest unambiguous tail of a logged path's FILE (`…/specs/e2e/articles.spec.ts` →
1411
1517
  * `e2e/articles.spec.ts`).
@@ -1512,9 +1618,11 @@ export function universalCause(log, found) {
1512
1618
  * bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a
1513
1619
  * single cause, two of them found by runs stopped before they walked the rest.
1514
1620
  *
1515
- * The signature is the failures' own error line where the log carries their bodies (a finished
1516
- * run, or a `line` reporter), and how long they took where it does not (a still-running `list`
1517
- * log). A test the log printed twice, or failed twice, is one test.
1621
+ * The signature is the cause itself where the log carries the failures' bodies (a finished run,
1622
+ * or a `line` reporter) — {@link failureMessage}: the error class plus the locator or the values
1623
+ * that differ — and how long they took where it does not (a still-running `list` log, which is
1624
+ * why a duration group is labelled as a duration and not as a proven cause). A test the log
1625
+ * printed twice, or failed twice, is one test.
1518
1626
  *
1519
1627
  * @param log - The log of a finished or running run.
1520
1628
  * @returns The causes, the one with the most tests first.
@@ -1576,11 +1684,15 @@ export function failureCausesNote(log) {
1576
1684
  return null;
1577
1685
  const listed = causes.slice(0, FAILURE_CAUSES_LISTED);
1578
1686
  const lines = listed.map((c, i) => {
1579
- const tests = c.tests
1580
- .slice(0, FAILURE_CAUSE_TESTS_LISTED)
1581
- .map((t) => shortWhere(t.where) || t.title);
1687
+ // The path the log itself wrote, never a shortened one: it is relative to the command's own
1688
+ // cwd, so it is the form the executor can paste back (X1 flash-m82 read a shortened
1689
+ // `e2e/auth.spec.ts:19` as the suite's own stale starter spec, rewrote its Playwright config
1690
+ // over it and deleted the app's tests — 4 minutes of a 60-minute run on a working config).
1691
+ const tests = c.tests.slice(0, FAILURE_CAUSE_TESTS_LISTED).map((t) => t.where || t.title);
1582
1692
  const more = c.tests.length > tests.length ? `, … and ${c.tests.length - tests.length} more` : '';
1583
- const signature = c.message !== null ? `\`${c.message}\`` : `each after ~${c.seconds}s`;
1693
+ const signature = c.message !== null
1694
+ ? `\`${c.message}\``
1695
+ : `each after ~${c.seconds}s (a shared duration, not a proven cause)`;
1584
1696
  const files = new Set(c.tests.map((t) => specFile(t.where)).filter(Boolean)).size;
1585
1697
  return (` ${i + 1}. ${signature} — ${c.tests.length} test${c.tests.length === 1 ? '' : 's'} ` +
1586
1698
  `in ${files} file${files === 1 ? '' : 's'}: ${tests.join(', ')}${more}`);
@@ -1611,12 +1723,26 @@ export function failureCausesNote(log) {
1611
1723
  */
1612
1724
  export function repeatingFailureNote(found, o) {
1613
1725
  const listed = found.tests.slice(0, REPEATING_FAILURE_LISTED);
1726
+ // Every row names its test by the SAME path the re-run hint below uses (`shortWhere`
1727
+ // would print this one file under two names: X1 flash-m82 read `✘ e2e/auth.spec.ts:19`
1728
+ // here against `Re-run fixtures/realworld/specs/e2e/auth.spec.ts:19 alone` three lines
1729
+ // down, took the suite-relative form for "the project's own stale starter spec" and
1730
+ // rewrote its Playwright config over it, deleted the app's tests and wrote its own —
1731
+ // 4 minutes of a 60-minute run, on a run whose config was right). The path the log
1732
+ // wrote is relative to the command's own cwd, so it is the one the executor can paste
1733
+ // back into a command.
1614
1734
  const rows = listed
1615
- .map((t) => ` ✘ ${[t.where && shortWhere(t.where), t.title].filter(Boolean).join(' › ')}` +
1735
+ .map((t) => ` ✘ ${[t.where, t.title].filter(Boolean).join(' › ')}` +
1616
1736
  (t.seconds === null ? '' : ` (${t.seconds}s)`))
1617
1737
  .join('\n');
1618
1738
  const more = found.count > listed.length ? `\n … and ${found.count - listed.length} more` : '';
1619
- const signature = found.message !== null
1739
+ // Two signatures, and only ONE of them is a cause. An error repeating verbatim is the cause
1740
+ // itself; a duration repeating is what a suite's own `actionTimeout` does to every failure that
1741
+ // waits for anything, so the note must not call it one cause (X1 flash-m84, 2026-10-16: four
1742
+ // failures at 6.8-7.4 s, four different missing elements, and this sentence told the executor
1743
+ // "one cause is repeating").
1744
+ const isCause = found.message !== null;
1745
+ const signature = isCause
1620
1746
  ? `every one with \`${found.message}\``
1621
1747
  : `each after ~${found.seconds}s — the same duration to within half a second`;
1622
1748
  const first = found.tests[0];
@@ -1637,14 +1763,21 @@ export function repeatingFailureNote(found, o) {
1637
1763
  `repeats this one cause. Stop it and fix that cause first: \`exec_command: ${o.stop}\`, ` +
1638
1764
  `then run the whole suite again. It is still going (${where}).${reproduce}`);
1639
1765
  }
1640
- return (`${found.count} different tests in this run have already failed the same way — ${signature} — ` +
1641
- `so one cause is repeating:\n${rows}${more}\n` +
1642
- `Start fixing it now, while the run keeps walking.${reproduce} ` +
1643
- `Let the rest of the run finish — it is the only thing that will name the causes that are NOT ` +
1766
+ const keepGoing = `Let the rest of the run finish — it is the only thing that will name the causes that are NOT ` +
1644
1767
  `this one, and a run stopped at its first repeating cause costs a second whole run to find ` +
1645
1768
  `them. Stop it only if every test it walks fails this same way (the app unreachable, a 5xx on ` +
1646
1769
  `every request, the same selector timing out on every test of every file so far). ` +
1647
- `It is still going (${where}).`);
1770
+ `It is still going (${where}).`;
1771
+ if (isCause)
1772
+ return (`${found.count} different tests in this run have already failed the same way — ${signature} — ` +
1773
+ `so one cause is repeating:\n${rows}${more}\n` +
1774
+ `Start fixing it now, while the run keeps walking.${reproduce} ${keepGoing}`);
1775
+ return (`${found.count} different tests in this run each failed after ~${found.seconds}s — the same ` +
1776
+ `duration to within half a second. A shared duration is a lead, not a cause: a suite whose ` +
1777
+ `config sets a timeout spends that timeout on every failure that waits for anything, so ` +
1778
+ `different missing elements land on the same number. Read these failures before fixing one ` +
1779
+ `of them:\n${rows}${more}\n` +
1780
+ `Start on them now, while the run keeps walking.${reproduce} ${keepGoing}`);
1648
1781
  }
1649
1782
  /**
1650
1783
  * Attempt a whitespace-tolerant replacement when an exact `old_string` match
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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",