@molecule/api-ai-tools 1.6.5 → 1.7.0

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,15 +3,11 @@ 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-16T19:48:43.608Z
6
+ Generated: 2026-10-17T00:30:38.264Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
10
10
 
11
- > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
- > It is written to be read by coding agents as much as by people, and is generated from this
13
- > package's source — edit `src/index.ts` JSDoc, not this file.
14
-
15
11
  Shared AI agent tool set for molecule.dev — filesystem, search, and shell
16
12
  tools an LLM agent can call, with a swappable execution backend.
17
13
 
@@ -24,6 +20,10 @@ WHERE they act: `createLocalBackend(projectRoot)` (host filesystem) or
24
20
  container). `buildAgentPrompt(ctx)` composes a matching system prompt
25
21
  (identity, tool listing, project docs, discovered skills).
26
22
 
23
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
24
+ > It is written to be read by coding agents as much as by people, and is generated from this
25
+ > package's source — edit `src/index.ts` JSDoc, not this file.
26
+
27
27
  ## Quick Start
28
28
 
29
29
  ```typescript
@@ -73,6 +73,37 @@ interface CommandSegment {
73
73
  args: readonly string[]
74
74
  /** The program and its arguments as one line — what a runner pattern is matched against. */
75
75
  text: string
76
+ /**
77
+ * The seconds the segment's own `timeout` wrapper allows the command it runs
78
+ * (`timeout 280 hurl --test …` → 280), or null when it carries none.
79
+ */
80
+ timeoutSeconds: number | null
81
+ }
82
+ ```
83
+
84
+ #### `CutRun`
85
+
86
+ A run a budget stopped, as its result reports it.
87
+
88
+ ```typescript
89
+ interface CutRun {
90
+ /** The bound the run was allowed, in seconds. */
91
+ seconds: number
92
+ /** Whether this tool's budget stopped it or the `timeout` the command carried. */
93
+ stoppedBy: CutBy
94
+ /**
95
+ * The exit code the run's own process reported, or null when the code in the result belongs to
96
+ * the pipeline's filter (`… | tail`) rather than to the run — the case that read as success.
97
+ */
98
+ exitCode: number | null
99
+ /** How many files or tests the runner's own output shows finishing, or null when it shows none. */
100
+ ran: number | null
101
+ /** How many the run was walking, when the runner printed it (Playwright's `[n/K]`), or null. */
102
+ total: number | null
103
+ /** The noun the runner counts in. */
104
+ noun: 'files' | 'tests'
105
+ /** The last thing the run printed — the line a reader looks at first. */
106
+ doing: string | null
76
107
  }
77
108
  ```
78
109
 
@@ -155,6 +186,51 @@ interface SuiteRunInFlight {
155
186
  }
156
187
  ```
157
188
 
189
+ #### `TraceRequest`
190
+
191
+ One request a traced page made, as `*.network` records it.
192
+
193
+ ```typescript
194
+ interface TraceRequest {
195
+ /** The HTTP method (`POST`). */
196
+ method: string
197
+ /** The request's path (`/api/articles/x/favorite`), or the whole URL when it is cross-origin. */
198
+ path: string
199
+ /** The response status, or 0 when the request never got a response. */
200
+ status: number
201
+ }
202
+ ```
203
+
204
+ #### `TraceArtifact`
205
+
206
+ What one trace says: the test it belongs to, the requests it made, and its console errors.
207
+
208
+ ```typescript
209
+ interface TraceArtifact {
210
+ /** The failure's spec file, relative to the run's directory, when the trace names one. */
211
+ spec: string | null
212
+ /** The line of the test's last traced step, 0 when the trace names no spec. */
213
+ line: number
214
+ /** The requests the page made, the interesting ones first ({@link traceArtifactsScript}). */
215
+ requests: TraceRequest[]
216
+ /** The console errors and page errors the page raised. */
217
+ console: string[]
218
+ }
219
+ ```
220
+
221
+ #### `DiagnosisTraceRewrite`
222
+
223
+ A command with the trace flag this platform decided to add, and the sentence to report it in.
224
+
225
+ ```typescript
226
+ interface DiagnosisTraceRewrite {
227
+ /** The command to run. */
228
+ command: string
229
+ /** One sentence naming the flag and why, or null when nothing was added. */
230
+ change: string | null
231
+ }
232
+ ```
233
+
158
234
  #### `ExecutionBackend`
159
235
 
160
236
  Abstraction over the execution environment.
@@ -443,6 +519,22 @@ interface FailureCause {
443
519
 
444
520
  ### Types
445
521
 
522
+ #### `CommandRunState`
523
+
524
+ What a command's result records: a run that ended, or one a budget stopped.
525
+
526
+ ```typescript
527
+ type CommandRunState = 'ran' | typeof CUT_AT_BUDGET
528
+ ```
529
+
530
+ #### `CutBy`
531
+
532
+ What stopped a run.
533
+
534
+ ```typescript
535
+ type CutBy = 'tool-budget' | 'command-timeout'
536
+ ```
537
+
446
538
  #### `DiscoveredSkill` _(deprecated)_
447
539
 
448
540
  Re-export SkillEntry as DiscoveredSkill for backwards compatibility.
@@ -491,6 +583,18 @@ function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): Execu
491
583
 
492
584
  **Returns:** A backend that proxies all filesystem calls into the sandbox.
493
585
 
586
+ #### `durationSeconds(word)`
587
+
588
+ The seconds a duration word stands for.
589
+
590
+ ```typescript
591
+ function durationSeconds(word: string): number | null
592
+ ```
593
+
594
+ - `word` — A duration (`280`, `20s`, `1m`).
595
+
596
+ **Returns:** The seconds, or null when the word is not a duration.
597
+
494
598
  #### `commandSegments(command)`
495
599
 
496
600
  The segments of a command, each with its program and arguments — the answer every classifier
@@ -504,6 +608,25 @@ function commandSegments(command: string): CommandSegment[]
504
608
 
505
609
  **Returns:** One entry per statement or pipeline stage that runs something, in order.
506
610
 
611
+ #### `commandTimeoutSeconds(command)`
612
+
613
+ The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
614
+ under (`timeout 280 hurl --test …` → 280), or null when it carries none.
615
+
616
+ This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
617
+ finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
618
+ other command reader uses, rather than matched as text: `timeout` in an argument
619
+ (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
620
+ the bound ({@link WRAPPER_VALUE_FLAGS}).
621
+
622
+ ```typescript
623
+ function commandTimeoutSeconds(command: string): number | null
624
+ ```
625
+
626
+ - `command` — The shell command.
627
+
628
+ **Returns:** The seconds the command allows itself, or null.
629
+
507
630
  #### `runsInCommandPosition(command, pattern)`
508
631
 
509
632
  Whether any segment of the command runs a program whose command-position text matches
@@ -522,6 +645,102 @@ function runsInCommandPosition(command: string, pattern: RegExp): boolean
522
645
 
523
646
  **Returns:** True when a segment in command position matches.
524
647
 
648
+ #### `commandRunState(result)`
649
+
650
+ The state a command's result records — THE reader every verdict about a run goes through.
651
+
652
+ A result that carries no state is a run that ended: the state is written where the run is
653
+ stopped ({@link commandCutRun}), so a result without it was never stopped by a budget this
654
+ platform set or can see. Results produced before this state existed read the same way, which is
655
+ what keeps a past turn's tally readable.
656
+
657
+ ```typescript
658
+ function commandRunState(result: unknown): CommandRunState
659
+ ```
660
+
661
+ - `result` — The exec_command / wait_for_task result.
662
+
663
+ **Returns:** The state the result records.
664
+
665
+ #### `suiteTally(text)`
666
+
667
+ Whether text carries a runner's closing summary ({@link SUITE_TALLY}).
668
+
669
+ ```typescript
670
+ function suiteTally(text: string): boolean
671
+ ```
672
+
673
+ - `text` — The run's stdout and stderr.
674
+
675
+ **Returns:** True when the runner's own end-of-run summary is there.
676
+
677
+ #### `commandCutRun(command, result, elapsedMs, budgetSeconds)`
678
+
679
+ The run a command's result records as stopped by a budget, or null when the result is a run that
680
+ ended (or says nothing about a run).
681
+
682
+ Evidence, in order — never a guess:
683
+
684
+ - `timeout`'s own 124 (the code the backend's wrapper returns whenever the TOOL's budget fired,
685
+ whatever the pipeline ended with) — and, when the clock says the command's own smaller bound
686
+ ran out first and less than the budget, that bound instead.
687
+ - the command carried its own `timeout <N>`, N is inside this tool's budget (a larger one never
688
+ fires — the tool's does), the run used N, and no runner printed a closing summary: the run was
689
+ killed at its own bound. Its exit code, when the pipeline handed one back, is the filter's.
690
+ - the run used this tool's whole budget behind a filter that withheld its output, and printed no
691
+ closing summary: the budget stopped it, and no code in the result is the run's.
692
+
693
+ ```typescript
694
+ function commandCutRun(
695
+ command: string,
696
+ result: unknown,
697
+ elapsedMs: number,
698
+ budgetSeconds?: number,
699
+ ): CutRun | null
700
+ ```
701
+
702
+ - `command` — The command the executor sent.
703
+ - `result` — Its result.
704
+ - `elapsedMs` — How long the command ran, in ms.
705
+ - `budgetSeconds` — This tool's foreground budget, in seconds (0 when it has none).
706
+
707
+ **Returns:** The stopped run, or null.
708
+
709
+ #### `cutRunFacts(result)`
710
+
711
+ The facts a result records about the run a budget stopped it in — what a reader needs to say
712
+ how far the run got WITHOUT re-reading its log or re-deriving the counts.
713
+
714
+ The numbers are read from the runner's own output where it printed them (Hurl's per-file lines,
715
+ Playwright's `[n/K]`), so a caller that can also see the suite itself — the file count of the
716
+ `*.hurl` directory the run walked — fills in the `total` this could not know, and both then say
717
+ the same sentence ({@link cutRunSentence}).
718
+
719
+ ```typescript
720
+ function cutRunFacts(result: unknown): CutRun | null
721
+ ```
722
+
723
+ - `result` — The exec_command / wait_for_task result.
724
+
725
+ **Returns:** The facts the result carries, or null when it records no stopped run.
726
+
727
+ #### `cutRunSentence(run)`
728
+
729
+ The first line of a stopped run's result: how long it was allowed, what stopped it, how far it
730
+ got, and the last thing it printed.
731
+
732
+ Written to be quotable by the model in one sentence and to leave no room for "nearly passing":
733
+ the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
734
+ read elsewhere (the suite's own file count) fills in the "M of K" it can then state.
735
+
736
+ ```typescript
737
+ function cutRunSentence(run: CutRun): string
738
+ ```
739
+
740
+ - `run` — The stopped run.
741
+
742
+ **Returns:** The sentence.
743
+
525
744
  #### `normalizeToolInput(schema, raw)`
526
745
 
527
746
  Move known aliases onto the declared parameter names and report what is still
@@ -851,6 +1070,107 @@ function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool
851
1070
 
852
1071
  **Returns:** Array of AITool objects ready to pass to an AI provider
853
1072
 
1073
+ #### `traceArtifactsScript()`
1074
+
1075
+ The Node program the sandbox runs to read a failed run's traces: `node <script> <dir>` prints one
1076
+ `TRACE <json>` line per `trace.zip` under `<dir>` (newest first, {@link MAX_TRACE_ARTIFACTS} of
1077
+ them), each with the test it belongs to, the requests its page made and its console errors.
1078
+
1079
+ It is a standalone program, not an import, for the reason `@molecule/api-code-sandbox` runs it:
1080
+ Playwright's own zip reader is not reachable from a project's `node_modules`, and a dependency
1081
+ on `unzip` would make the read fail on every sandbox that has none. The zip is read with the
1082
+ central directory and `zlib.inflateRawSync` — about forty lines, and the only thing that has to
1083
+ be true of a `trace.zip` is that it IS one.
1084
+
1085
+ A request is kept over a static asset when it failed (`status >= 400`) or never answered
1086
+ (`status === 0`), and over anything else when it is the page talking to the app
1087
+ (`fetch`/`xhr`/`document`): a failing test's trace holds the whole page load, and the answer is
1088
+ always at the end of it.
1089
+
1090
+ ```typescript
1091
+ function traceArtifactsScript(): string
1092
+ ```
1093
+
1094
+ **Returns:** The program's source, to write to a file and run.
1095
+
1096
+ #### `parseTraceArtifacts(stdout)`
1097
+
1098
+ The traces {@link traceArtifactsScript} printed, parsed.
1099
+
1100
+ A line that is not a complete `TRACE <json>` (a truncated read, a program that died mid-write) is
1101
+ dropped: a partial trace is not evidence about the test it came from.
1102
+
1103
+ ```typescript
1104
+ function parseTraceArtifacts(stdout: string): TraceArtifact[]
1105
+ ```
1106
+
1107
+ - `stdout` — The program's output.
1108
+
1109
+ **Returns:** One entry per trace it printed.
1110
+
1111
+ #### `failureTraceNote(items, target)`
1112
+
1113
+ The note a failed suite run's result carries: what each failing test's page actually sent, and
1114
+ what came back.
1115
+
1116
+ This is the answer to the question a swallowed error hides — a click that changed nothing is
1117
+ usually a request that failed — so the note leads with the failing requests and says what to do
1118
+ with them: read the handler the route reaches, not the selector. It is BOUNDED to
1119
+ {@link MAX_TRACE_NOTE_TESTS} tests with {@link MAX_TRACE_NOTE_REQUESTS} requests each, because it
1120
+ rides in a result the model pays for.
1121
+
1122
+ ```typescript
1123
+ function failureTraceNote(items: readonly TraceArtifact[], target: string): string | null
1124
+ ```
1125
+
1126
+ - `items` — The traces {@link parseTraceArtifacts} read.
1127
+ - `target` — Where the run went — the deployed host, as the note names it.
1128
+
1129
+ **Returns:** The note, or null when no trace held a request or a console error.
1130
+
1131
+ #### `suiteRecordsTraces(configText)`
1132
+
1133
+ Whether a Playwright config records traces for the tests that fail.
1134
+
1135
+ Only `on` and `retain-on-failure` keep a `trace.zip` for a failure whatever the retries do: the
1136
+ two retry modes need a retry to happen at all, and a diagnosis run is given `--retries=0`
1137
+ (`streamingBackgroundCommand`), so on X1 xr-molecule-flash-m100 the run's own
1138
+ `trace: 'on-first-retry'` recorded nothing — which is what left the failing tests' network log
1139
+ unreadable. A config that sets no `trace:` at all records nothing either (Playwright's default
1140
+ is off); so does one that cannot be read.
1141
+
1142
+ ```typescript
1143
+ function suiteRecordsTraces(configText: string): boolean
1144
+ ```
1145
+
1146
+ - `configText` — The config's source, or '' when none could be read.
1147
+
1148
+ **Returns:** True when the config records traces for failures.
1149
+
1150
+ #### `diagnosisTraceCommand(args)`
1151
+
1152
+ The command a DIAGNOSIS run needs to leave a readable trace behind, when the suite's config
1153
+ would record none.
1154
+
1155
+ The failing tests' requests are only readable if the run wrote them down (see the module doc),
1156
+ and the config is where the decision about that lives — the platform does not edit the suite's
1157
+ config, so it passes the flag instead, and only where it can do no harm: on a run narrowed to a
1158
+ part of the suite or capped ({@link isDiagnosisRun}), never on the whole-suite proof run, and
1159
+ never over a `--trace` the executor wrote itself. A capped walk is exactly the run whose
1160
+ failures someone is about to work on; the proof run is graded as it is.
1161
+
1162
+ ```typescript
1163
+ function diagnosisTraceCommand(args: {
1164
+ command: string
1165
+ cwd: string
1166
+ readFile: (path: string) => Promise<string>
1167
+ }): Promise<DiagnosisTraceRewrite>
1168
+ ```
1169
+
1170
+ - `args` — The command, the directory it starts in, and the backend's file read.
1171
+
1172
+ **Returns:** The command to run and the sentence to report, or the command unchanged.
1173
+
854
1174
  #### `shellQuote(s)`
855
1175
 
856
1176
  Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
@@ -1199,6 +1519,105 @@ function findPatternArgs(pattern: string): string
1199
1519
 
1200
1520
  **Returns:** A `find` predicate: `-name '…'` for a bare name, else one or more `-path '…'` alternatives, ORed inside parens when there is more than one.
1201
1521
 
1522
+ #### `findPathArgs(pattern)`
1523
+
1524
+ The `-path` alternatives of {@link findPatternArgs}, for a pattern whose
1525
+ directory the caller has already rooted the search at.
1526
+
1527
+ `find_files` narrows the search root itself when the pattern spells its own
1528
+ directory out (see {@link literalDirPrefix}) — `node_modules` plus
1529
+ `@molecule/api-resource-user/dist/*.d.ts` searched that one directory. The
1530
+ remainder (`*.d.ts`) must then keep the `-path` form rather than collapsing to
1531
+ `-name`: `*` crosses `/` in an fnmatch glob, so the `-path` form still finds a
1532
+ declaration file in a `dist/utilities/` subdirectory, which `-name '*.d.ts'`
1533
+ would drop (a silent partial answer, which is worse than the timeout it
1534
+ replaced).
1535
+
1536
+ ```typescript
1537
+ function findPathArgs(pattern: string): string
1538
+ ```
1539
+
1540
+ - `pattern` — The pattern's remainder, with a `/` in it or not.
1541
+
1542
+ **Returns:** One or more `-path '…'` alternatives, ORed inside parens when there is more than one.
1543
+
1544
+ #### `literalDirPrefix(pattern)`
1545
+
1546
+ The directory a GLOB pattern spells out, when it spells one out: everything
1547
+ before the pattern's first glob character, trimmed back to the last `/`.
1548
+
1549
+ A pattern is matched against the whole path with a leading "any parent"
1550
+ anchor ({@link findPatternArgs}), so `find` visits every entry below the
1551
+ search root. Handed `/workspace/node_modules` and
1552
+ `@molecule/api-resource-user/dist/*.d.ts` — a directory named outright —
1553
+ `find` walked the sandbox's entire dependency tree and hit the tool's 10 s
1554
+ deadline without printing a result (xr-molecule-flash-m94: the one directory
1555
+ the model meant answered a `list_files` in 0 s). The literal prefix is that
1556
+ directory, and rooting the search at it is what keeps the call instant.
1557
+
1558
+ A BARE first segment (`helpers/*`, `bonds/*`, `e2e/*.spec.ts`) is NOT one:
1559
+ `helpers/*` means "a `helpers/` directory anywhere below the root", which the
1560
+ tool documents and a live run relied on (xr-molecule-flash-m77 — a path
1561
+ pattern is not anchored to one depth). Rooting those at
1562
+ `<root>/helpers` would answer with a SUBSET, silently: the nested
1563
+ `api/src/helpers/` matches would disappear, and a missing file reads as
1564
+ "no such file". So a prefix is returned only when it names a path of two or
1565
+ more segments (`@molecule/api-resource-user/dist`), which is the shape that
1566
+ is a directory rather than a directory name.
1567
+
1568
+ ```typescript
1569
+ function literalDirPrefix(pattern: string): string | null
1570
+ ```
1571
+
1572
+ - `pattern` — A pattern already accepted by {@link isValidGlob}.
1573
+
1574
+ **Returns:** The literal directory prefix, without a trailing slash, or null when the pattern has no glob character, no `/`, or only a bare name before one.
1575
+
1576
+ #### `isCommandTimeout(error)`
1577
+
1578
+ Whether a backend error is the sandbox's own command deadline.
1579
+
1580
+ The E2B SDK answers a timed-out `exec` with its generic text —
1581
+ `[deadline_exceeded] the operation timed out: … It can be modified by passing
1582
+ 'timeoutMs' when making the request. Use '0' to disable the timeout.` — which
1583
+ names an option the executor cannot set and says nothing about the search
1584
+ that died. Handing that sentence to the model is what xr-molecule-flash-m94
1585
+ read after `find_files` spent 10 s walking `node_modules`; the tool answers
1586
+ the deadline in its own words instead (see {@link searchTimeoutError}).
1587
+
1588
+ ```typescript
1589
+ function isCommandTimeout(error: unknown): boolean
1590
+ ```
1591
+
1592
+ - `error` — The error the backend threw.
1593
+
1594
+ **Returns:** True when the error reads as a timeout or a deadline.
1595
+
1596
+ #### `searchTimeoutError(tool, searched, pattern, seconds, hint)`
1597
+
1598
+ The answer a search tool gives when the backend's deadline fires: what was
1599
+ searched, for what, how long the tool allowed, and the two ways to get an
1600
+ answer — a narrower `path`, or a listing. Never the vendor's `timeoutMs`
1601
+ sentence (see {@link isCommandTimeout}).
1602
+
1603
+ ```typescript
1604
+ function searchTimeoutError(
1605
+ tool: string,
1606
+ searched: string,
1607
+ pattern: string,
1608
+ seconds: number,
1609
+ hint: string,
1610
+ ): string
1611
+ ```
1612
+
1613
+ - `tool` — The tool that timed out (`find_files`, `search_files`).
1614
+ - `searched` — The directory the search actually covered.
1615
+ - `pattern` — The pattern it was searching for.
1616
+ - `seconds` — The tool's own limit, in seconds.
1617
+ - `hint` — What to do differently — the narrower target or the listing tool.
1618
+
1619
+ **Returns:** The message's text.
1620
+
1202
1621
  #### `pathArgError(path, tool)`
1203
1622
 
1204
1623
  Validate a file tool's `path` argument is a non-empty string. A weak model
@@ -1388,10 +1807,12 @@ function failureBlocks(s: string): string[]
1388
1807
  The shell command whose stdout is the interesting part of a log that is still being written:
1389
1808
  the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
1390
1809
 
1391
- A Playwright `list` run prints one line per test as it finishes and its error blocks only at
1392
- the end, so the LAST bytes of a running log are its least informative part — in X1 flash-m61
1393
- the last 3 000 bytes of a 139-test walk were the skip list, and all 61 failures were above
1394
- them, which cost the run the grep it had to make anyway.
1810
+ A Playwright `list` run prints one line per test as it finishes, so the LAST bytes of a running
1811
+ log are its least informative part — in X1 flash-m61 the last 3 000 bytes of a 139-test walk
1812
+ were the skip list, and all 61 failures were above them, which cost the run the grep it had to
1813
+ make anyway. A background run also carries each failure's own error as it fails
1814
+ ({@link PLAYWRIGHT_FAILURES_INLINE_ENV}), and {@link RUNNING_FAILURE_LINE_RE} already matches
1815
+ those blocks' headers and their `Error:` lines, so this slice shows the causes too.
1395
1816
 
1396
1817
  ```typescript
1397
1818
  function runningOutputSliceCommand(log: string, own?: string): string
@@ -1427,9 +1848,11 @@ function runningCauseCommand(log: string, own?: string): string
1427
1848
  The cause repeating among a running suite's failures, or null.
1428
1849
 
1429
1850
  Playwright's `list` reporter prints one line per test as it finishes — its mark, its title
1430
- path and how long it ran — and defers each failure's error to the summary at the end, so
1431
- mid-run the only signature those lines carry is the duration. Where the log carries the
1432
- failure bodies (a `line` reporter's output, or a finished run's summary) the signature is the
1851
+ path and how long it ran — and, on a run the platform started in the background, each failure's
1852
+ own error with it ({@link PLAYWRIGHT_FAILURES_INLINE_ENV}); a `list` run with no error bodies in
1853
+ its log (an older Playwright, a log the bodies have been cut from) carries only the duration as
1854
+ a signature. Where the log carries the failure bodies (either inline, or in a finished run's
1855
+ summary) the signature is the
1433
1856
  error itself — {@link failureMessage}: the error class plus the locator or the values that
1434
1857
  differ — which is stronger (it needs no assumption about how long a failure should take), so
1435
1858
  the message is looked for first and the duration is then left with the tests whose bodies the
@@ -1442,8 +1865,8 @@ each was the test timeout and one missing `navigate('/')` was the cause of all o
1442
1865
 
1443
1866
  The quorum is of DISTINCT tests — {@link REPEATING_FAILURE_MIN} of them — because a log that
1444
1867
  prints each failure twice (a second reporter, a retry) had X1 flash-m83 told that four of its
1445
- tests had failed the same way when two had, so the executor stopped a 139-test walk at test 21
1446
- with one cause in hand (2026-10-16).
1868
+
1869
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/utilities.d.ts`.)_
1447
1870
 
1448
1871
  ```typescript
1449
1872
  function repeatingFailure(log: string): RepeatingFailure | null
@@ -1478,6 +1901,43 @@ function universalCause(log: string, found: RepeatingFailure): UniversalCause |
1478
1901
 
1479
1902
  **Returns:** The cause's count and files, or null when the run is not failing whole.
1480
1903
 
1904
+ #### `earlyReturnCause(found, universal)`
1905
+
1906
+ The ONE finding that ends a wait early, and the only one a note may hand a stop command over
1907
+ for: a PROVEN cause that is failing the whole run.
1908
+
1909
+ Both halves are load-bearing, each of them learned from a run that lost minutes to the other.
1910
+
1911
+ - **Failing the WHOLE run** is {@link universalCause}: every failure the log names, across
1912
+ {@link UNIVERSAL_CAUSE_FILES} spec files or more, with nothing passing since the first — the
1913
+ dont-stop-runs rule. X1 flash-m83 (2026-10-16) stopped eight local runs at their first
1914
+ repeating cause in 14.5 minutes and never finished one, and a run that still has other causes
1915
+ left to name is the only thing that will name them.
1916
+ - **A PROVEN cause** is an error repeated VERBATIM. A shared DURATION is a LEAD, never a cause:
1917
+ a suite whose config sets a timeout spends that timeout on every failure that waits for
1918
+ anything, so three unrelated tests timing out together look exactly like one broken run
1919
+ (`~7 s` is a property of the CONFIG, not of the cause — {@link normalizedSignature}). X1
1920
+ xr-molecule-flash-m99 (2026-10-16) was told `status: 'repeating_failure'` twice on that
1921
+ shape — three `url-navigation.spec.ts` tests at ~16.2 s, one `timeout: 15_000` plus teardown
1922
+ — read the early return as "stop waiting, edit now", and voided a run that was still walking;
1923
+ the second cost a 224 s wait that ended with no result at all.
1924
+
1925
+ Neither half alone is enough, and {@link repeatingFailure} finding a signature is not either:
1926
+
1927
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/utilities.d.ts`.)_
1928
+
1929
+ ```typescript
1930
+ function earlyReturnCause(
1931
+ found: RepeatingFailure,
1932
+ universal: UniversalCause | null | undefined,
1933
+ ): UniversalCause | null
1934
+ ```
1935
+
1936
+ - `found` — What {@link repeatingFailure} found mid-run.
1937
+ - `universal` — What {@link universalCause} found for it, if anything.
1938
+
1939
+ **Returns:** The cause to stop the run over, or null — in which case nothing may be stopped, the wait keeps going, and the finding rides the run's result as a note.
1940
+
1481
1941
  #### `failuresByCause(log)`
1482
1942
 
1483
1943
  Every failure a run names, grouped by the cause behind it — one group per cause, with the
@@ -1488,11 +1948,11 @@ after another with no statement of which of them share a cause, so a run of 19 f
1488
1948
  bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a
1489
1949
  single cause, two of them found by runs stopped before they walked the rest.
1490
1950
 
1491
- The signature is the cause itself where the log carries the failures' bodies (a finished run,
1492
- or a `line` reporter) — {@link failureMessage}: the error class plus the locator or the values
1493
- that differ — and how long they took where it does not (a still-running `list` log, which is
1494
- why a duration group is labelled as a duration and not as a proven cause). A test the log
1495
- printed twice, or failed twice, is one test.
1951
+ The signature is the cause itself where the log carries the failures' bodies (a finished run, a
1952
+ background `list` run — whose failures print as they fail — or a `line` reporter) —
1953
+ {@link failureMessage}: the error class plus the locator or the values that differ — and how long
1954
+ they took where it does not, which is why a duration group is labelled as a duration and not as a
1955
+ proven cause. A test the log printed twice, or failed twice, is one test.
1496
1956
 
1497
1957
  ```typescript
1498
1958
  function failuresByCause(log: string): FailureCause[]
@@ -1553,18 +2013,19 @@ what that function could actually see in the log — including the count, which
1553
2013
  tests, so the note never names one or two tests as a cause.
1554
2014
 
1555
2015
  The note names the cause early so the executor can START FIXING while the run keeps walking,
1556
- and it asks for the run to be stopped only when {@link universalCause} says the cause is
1557
- failing the whole run. X1 flash-m76 (2026-10-16) spent 4.0 minutes walking 15 tests whose one
1558
- cause was certain at the third, 47 s in, and the note answers exactly that; X1 flash-m83 then
1559
- stopped eight runs at their first repeating cause in 14.5 minutes and never finished one, so
1560
- the note must not say to stop when the run still has other causes to name.
2016
+ and it hands a stop command over only when {@link earlyReturnCause} says so — the SAME
2017
+ predicate the wait uses to decide whether to return early, so a finding the wait keeps waiting
2018
+ on is a finding this note never asks to stop for. X1 flash-m76 (2026-10-16) spent 4.0 minutes
2019
+ walking 15 tests whose one cause was certain at the third, 47 s in, and the note answers
2020
+ exactly that; X1 flash-m83 then stopped eight runs at their first repeating cause in 14.5
2021
+ minutes and never finished one, so the note must not say to stop when the run still has other
2022
+ causes to name. X1 xr-molecule-flash-m99 (2026-10-16) is the third shape: a shared duration is
2023
+ not a cause at all, so a lead reaches the executor as a note on a result it is still waiting
2024
+ for — never as a verdict with a stop command in it.
1561
2025
 
1562
2026
  The run is STILL WALKING while this note is written — both callers produce it mid-run — so a
1563
2027
  re-run of one of its tests is a second run against the same app and database, and the platform
1564
2028
  refuses that ({@link suiteRunInTheWay}, the same rule its overlap hold answers with):
1565
- xr-molecule-flash-m89 (2026-10-16) read _"Start on them now, while the run keeps walking.
1566
- `null-fields.spec.ts:53` alone reproduces the first one."_ and sent exactly that re-run 23 s
1567
- later, where the hold answered *"A playwright suite is already running against this app and
1568
2029
 
1569
2030
  _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/utilities.d.ts`.)_
1570
2031
 
@@ -1582,7 +2043,7 @@ function repeatingFailureNote(
1582
2043
  ```
1583
2044
 
1584
2045
  - `found` — What {@link repeatingFailure} found.
1585
- - `o` — The run's facts: its `taskId`, its `log` path, `stop` — the shell command that stops it, printed only for a {@link UniversalCause} — `universal`, what {@link universalCause} found (absent means the run is not failing whole), and `run`, how the run was started ({@link rerunOfRunningSuite}) when the caller knows it.
2046
+ - `o` — The run's facts: its `taskId`, its `log` path, `stop` — the shell command that stops it, printed only for an {@link earlyReturnCause} — `universal`, what {@link universalCause} found (absent means the run is not failing whole), and `run`, how the run was started ({@link rerunOfRunningSuite}) when the caller knows it.
1586
2047
 
1587
2048
  **Returns:** The note.
1588
2049
 
@@ -1664,6 +2125,15 @@ function fusedLine(content: string, newString: string): { line: number; text: st
1664
2125
 
1665
2126
  ### Constants
1666
2127
 
2128
+ #### `CUT_AT_BUDGET`
2129
+
2130
+ The state a result carries when a budget stopped its command: a run that did not finish is not a
2131
+ run that passed, and no reader may treat it as one.
2132
+
2133
+ ```typescript
2134
+ const CUT_AT_BUDGET: 'cut_at_budget'
2135
+ ```
2136
+
1667
2137
  #### `TOOL_SCHEMAS`
1668
2138
 
1669
2139
  Canonical tool schemas shared by the agent runtime and documentation.
@@ -1672,6 +2142,108 @@ Canonical tool schemas shared by the agent runtime and documentation.
1672
2142
  const TOOL_SCHEMAS: Record<string, ToolSchema>
1673
2143
  ```
1674
2144
 
2145
+ #### `SPLIT_WORK_ADVICE`
2146
+
2147
+ The closing advice on an over-budget message for a command that is not a test
2148
+ suite: the work it was chaining is what to split.
2149
+
2150
+ ```typescript
2151
+ const SPLIT_WORK_ADVICE: string
2152
+ ```
2153
+
2154
+ #### `SUITE_OVER_BUDGET_ADVICE`
2155
+
2156
+ The closing advice on an over-budget message for a command that IS a suite run
2157
+ (Playwright, Vitest, a `hurl` run — {@link isSuiteRunCommand}): its files are not
2158
+ a bigger unit to break up.
2159
+
2160
+ X1 flash-m36 asked for this half and the message kept saying "run a smaller unit"
2161
+ for two months. It is the wrong direction for a suite, and measurably: a run
2162
+ stopped here reports NO tally, so nothing in the result says which test or request
2163
+ hung — the run has to be sent again in the background (off this budget, its output
2164
+ written to a log that can be read mid-run) or, when a consumer knows the runner's
2165
+ per-request limit, with each request bounded. xr-molecule-flash-m93 (+06:22,
2166
+ 2026-10-16) lost 4.8 minutes to a Hurl run whose ONE unanswered request had hurl's
2167
+ 300 s default per request holding the file, ten seconds past this tool's budget.
2168
+
2169
+ ```typescript
2170
+ const SUITE_OVER_BUDGET_ADVICE: string
2171
+ ```
2172
+
2173
+ #### `MAX_TRACE_ARTIFACTS`
2174
+
2175
+ How many traces one read reports, newest first.
2176
+
2177
+ ```typescript
2178
+ const MAX_TRACE_ARTIFACTS: 12
2179
+ ```
2180
+
2181
+ #### `MAX_TRACED_REQUESTS`
2182
+
2183
+ How many requests one test's trace contributes.
2184
+
2185
+ ```typescript
2186
+ const MAX_TRACED_REQUESTS: 6
2187
+ ```
2188
+
2189
+ #### `MAX_TRACED_CONSOLE`
2190
+
2191
+ How many console errors one test's trace contributes.
2192
+
2193
+ ```typescript
2194
+ const MAX_TRACED_CONSOLE: 4
2195
+ ```
2196
+
2197
+ #### `MAX_TRACE_NOTE_TESTS`
2198
+
2199
+ How many tests the note in a run's result names.
2200
+
2201
+ ```typescript
2202
+ const MAX_TRACE_NOTE_TESTS: 5
2203
+ ```
2204
+
2205
+ #### `MAX_TRACE_NOTE_REQUESTS`
2206
+
2207
+ How many requests the note names per test.
2208
+
2209
+ ```typescript
2210
+ const MAX_TRACE_NOTE_REQUESTS: 4
2211
+ ```
2212
+
2213
+ #### `TRACE_NOTE_URL_CHARS`
2214
+
2215
+ How long a URL the note prints, before the middle is cut.
2216
+
2217
+ ```typescript
2218
+ const TRACE_NOTE_URL_CHARS: 96
2219
+ ```
2220
+
2221
+ #### `DIAGNOSIS_TRACE_FLAG`
2222
+
2223
+ The `--trace` flag this platform adds, and the only value it adds it with.
2224
+
2225
+ ```typescript
2226
+ const DIAGNOSIS_TRACE_FLAG: '--trace=retain-on-failure'
2227
+ ```
2228
+
2229
+ #### `PLAYWRIGHT_FAILURES_INLINE_ENV`
2230
+
2231
+ Playwright's switch for printing each failure's own error as that test fails, instead of only in
2232
+ the summary at the end: `list` reads it in its constructor (`ListReporterOptions.printFailuresInline`,
2233
+ `playwright/lib/reporters/list.js` — 1.63, the version the toolchain pins), and when it prints
2234
+ inline the end-of-run repeat is suppressed with it (`epilogue(!printFailuresInline)`), so one
2235
+ reporter still prints each failure exactly once.
2236
+
2237
+ A background suite is put on `list` (see {@link streamingBackgroundCommand}) and this with it, so
2238
+ the log names its causes WHILE it walks. Without it the log's only failure lines are `✘` rows:
2239
+ X1 xr-molecule-flash-m99 (+19:27 → +21:00, 2026-10-16) was told to `grep -n -A 20 '<test name>'`
2240
+ the log, got nothing back through four greps, inferred causes from the spec prose instead, and
2241
+ edited `app/src` while the run was still walking — which voids the run.
2242
+
2243
+ ```typescript
2244
+ const PLAYWRIGHT_FAILURES_INLINE_ENV: 'PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE'
2245
+ ```
2246
+
1675
2247
  #### `MAX_READ_SIZE`
1676
2248
 
1677
2249
  Max file size for read_file (5MB).