@molecule/api-ai-tools 1.6.4 → 1.6.6

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-16T16:49:27.760Z
6
+ Generated: 2026-10-16T21:18:13.332Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -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
 
@@ -132,6 +163,29 @@ interface ToolSchema {
132
163
  }
133
164
  ```
134
165
 
166
+ #### `SuiteRunInFlight`
167
+
168
+ One suite run, as the rule that decides whether a second may start beside it reads it.
169
+
170
+ Both callers describe the runs in flight the same way and ask the same question of them; what
171
+ differs is only which runs each one knows about — the hold reads its ledger of background runs,
172
+ the note reads the one run it is waiting on.
173
+
174
+ ```typescript
175
+ interface SuiteRunInFlight {
176
+ /** Whether the run invokes a suite at all: a `vitest`/`jest` run is not one, and cannot be. */
177
+ runsSuite: boolean
178
+ /** The app and database the run measures, as {@link suiteCommandTarget} keys it. */
179
+ target: string
180
+ /**
181
+ * Whether the command being judged STOPS this run before starting its own — a kill comes first,
182
+ * so a run the command itself stops is not in the way (xr-molecule-flash-m87's
183
+ * `kill 19408 … ; npx playwright test …`).
184
+ */
185
+ stoppedByCommand?: boolean
186
+ }
187
+ ```
188
+
135
189
  #### `ExecutionBackend`
136
190
 
137
191
  Abstraction over the execution environment.
@@ -420,6 +474,22 @@ interface FailureCause {
420
474
 
421
475
  ### Types
422
476
 
477
+ #### `CommandRunState`
478
+
479
+ What a command's result records: a run that ended, or one a budget stopped.
480
+
481
+ ```typescript
482
+ type CommandRunState = 'ran' | typeof CUT_AT_BUDGET
483
+ ```
484
+
485
+ #### `CutBy`
486
+
487
+ What stopped a run.
488
+
489
+ ```typescript
490
+ type CutBy = 'tool-budget' | 'command-timeout'
491
+ ```
492
+
423
493
  #### `DiscoveredSkill` _(deprecated)_
424
494
 
425
495
  Re-export SkillEntry as DiscoveredSkill for backwards compatibility.
@@ -468,6 +538,18 @@ function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): Execu
468
538
 
469
539
  **Returns:** A backend that proxies all filesystem calls into the sandbox.
470
540
 
541
+ #### `durationSeconds(word)`
542
+
543
+ The seconds a duration word stands for.
544
+
545
+ ```typescript
546
+ function durationSeconds(word: string): number | null
547
+ ```
548
+
549
+ - `word` — A duration (`280`, `20s`, `1m`).
550
+
551
+ **Returns:** The seconds, or null when the word is not a duration.
552
+
471
553
  #### `commandSegments(command)`
472
554
 
473
555
  The segments of a command, each with its program and arguments — the answer every classifier
@@ -481,6 +563,25 @@ function commandSegments(command: string): CommandSegment[]
481
563
 
482
564
  **Returns:** One entry per statement or pipeline stage that runs something, in order.
483
565
 
566
+ #### `commandTimeoutSeconds(command)`
567
+
568
+ The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
569
+ under (`timeout 280 hurl --test …` → 280), or null when it carries none.
570
+
571
+ This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
572
+ finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
573
+ other command reader uses, rather than matched as text: `timeout` in an argument
574
+ (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
575
+ the bound ({@link WRAPPER_VALUE_FLAGS}).
576
+
577
+ ```typescript
578
+ function commandTimeoutSeconds(command: string): number | null
579
+ ```
580
+
581
+ - `command` — The shell command.
582
+
583
+ **Returns:** The seconds the command allows itself, or null.
584
+
484
585
  #### `runsInCommandPosition(command, pattern)`
485
586
 
486
587
  Whether any segment of the command runs a program whose command-position text matches
@@ -499,6 +600,102 @@ function runsInCommandPosition(command: string, pattern: RegExp): boolean
499
600
 
500
601
  **Returns:** True when a segment in command position matches.
501
602
 
603
+ #### `commandRunState(result)`
604
+
605
+ The state a command's result records — THE reader every verdict about a run goes through.
606
+
607
+ A result that carries no state is a run that ended: the state is written where the run is
608
+ stopped ({@link commandCutRun}), so a result without it was never stopped by a budget this
609
+ platform set or can see. Results produced before this state existed read the same way, which is
610
+ what keeps a past turn's tally readable.
611
+
612
+ ```typescript
613
+ function commandRunState(result: unknown): CommandRunState
614
+ ```
615
+
616
+ - `result` — The exec_command / wait_for_task result.
617
+
618
+ **Returns:** The state the result records.
619
+
620
+ #### `suiteTally(text)`
621
+
622
+ Whether text carries a runner's closing summary ({@link SUITE_TALLY}).
623
+
624
+ ```typescript
625
+ function suiteTally(text: string): boolean
626
+ ```
627
+
628
+ - `text` — The run's stdout and stderr.
629
+
630
+ **Returns:** True when the runner's own end-of-run summary is there.
631
+
632
+ #### `commandCutRun(command, result, elapsedMs, budgetSeconds)`
633
+
634
+ The run a command's result records as stopped by a budget, or null when the result is a run that
635
+ ended (or says nothing about a run).
636
+
637
+ Evidence, in order — never a guess:
638
+
639
+ - `timeout`'s own 124 (the code the backend's wrapper returns whenever the TOOL's budget fired,
640
+ whatever the pipeline ended with) — and, when the clock says the command's own smaller bound
641
+ ran out first and less than the budget, that bound instead.
642
+ - the command carried its own `timeout <N>`, N is inside this tool's budget (a larger one never
643
+ fires — the tool's does), the run used N, and no runner printed a closing summary: the run was
644
+ killed at its own bound. Its exit code, when the pipeline handed one back, is the filter's.
645
+ - the run used this tool's whole budget behind a filter that withheld its output, and printed no
646
+ closing summary: the budget stopped it, and no code in the result is the run's.
647
+
648
+ ```typescript
649
+ function commandCutRun(
650
+ command: string,
651
+ result: unknown,
652
+ elapsedMs: number,
653
+ budgetSeconds?: number,
654
+ ): CutRun | null
655
+ ```
656
+
657
+ - `command` — The command the executor sent.
658
+ - `result` — Its result.
659
+ - `elapsedMs` — How long the command ran, in ms.
660
+ - `budgetSeconds` — This tool's foreground budget, in seconds (0 when it has none).
661
+
662
+ **Returns:** The stopped run, or null.
663
+
664
+ #### `cutRunFacts(result)`
665
+
666
+ The facts a result records about the run a budget stopped it in — what a reader needs to say
667
+ how far the run got WITHOUT re-reading its log or re-deriving the counts.
668
+
669
+ The numbers are read from the runner's own output where it printed them (Hurl's per-file lines,
670
+ Playwright's `[n/K]`), so a caller that can also see the suite itself — the file count of the
671
+ `*.hurl` directory the run walked — fills in the `total` this could not know, and both then say
672
+ the same sentence ({@link cutRunSentence}).
673
+
674
+ ```typescript
675
+ function cutRunFacts(result: unknown): CutRun | null
676
+ ```
677
+
678
+ - `result` — The exec_command / wait_for_task result.
679
+
680
+ **Returns:** The facts the result carries, or null when it records no stopped run.
681
+
682
+ #### `cutRunSentence(run)`
683
+
684
+ The first line of a stopped run's result: how long it was allowed, what stopped it, how far it
685
+ got, and the last thing it printed.
686
+
687
+ Written to be quotable by the model in one sentence and to leave no room for "nearly passing":
688
+ the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
689
+ read elsewhere (the suite's own file count) fills in the "M of K" it can then state.
690
+
691
+ ```typescript
692
+ function cutRunSentence(run: CutRun): string
693
+ ```
694
+
695
+ - `run` — The stopped run.
696
+
697
+ **Returns:** The sentence.
698
+
502
699
  #### `normalizeToolInput(schema, raw)`
503
700
 
504
701
  Move known aliases onto the declared parameter names and report what is still
@@ -606,6 +803,108 @@ function commandRunsNoTests(command: string): boolean
606
803
 
607
804
  **Returns:** True when the command invokes a runner only to print.
608
805
 
806
+ #### `withoutHeredocs(command)`
807
+
808
+ A command with its heredoc bodies removed: the runs, kills and hosts it names in a file it
809
+ WRITES are not what the command itself does.
810
+
811
+ The header is kept (`cat > run.sh <<'EOF' …EOF` leaves `cat > run.sh`), which is what the
812
+ readers here want: everything before the body is still readable as the command's own words.
813
+
814
+ ```typescript
815
+ function withoutHeredocs(command: string): string
816
+ ```
817
+
818
+ - `command` — The shell command.
819
+
820
+ **Returns:** The command without its heredoc bodies.
821
+
822
+ #### `suiteCommandTarget(command)`
823
+
824
+ The server a suite command points at, as a comparable key: the host it names (`HOST=`,
825
+ `BASE_URL=`, `PLAYWRIGHT_BASE_URL=`, `--variable host=`, or a bare URL), or `local` for a run
826
+ that names none — a suite run in the sandbox goes to the sandbox's own app unless the command
827
+ says otherwise. Every localhost spelling collapses to `local` (one sandbox, one app, one
828
+ database).
829
+
830
+ ```typescript
831
+ function suiteCommandTarget(command: string): string
832
+ ```
833
+
834
+ - `command` — The suite command.
835
+
836
+ **Returns:** The target key.
837
+
838
+ #### `scopedRerunCommand(command, locations)`
839
+
840
+ The whole-suite command made scoped to the tests it should re-run instead: the same
841
+ invocation — same directory, same settings — with the named `file:line`s in place of the whole
842
+ suite, `--last-failed` dropped (it re-runs the tests that never ran too, X1 flash-m60) and
843
+ `--retries=0` set (a re-run of a known failure does not need to pay for retries).
844
+
845
+ ```typescript
846
+ function scopedRerunCommand(command: string, locations: readonly string[]): string
847
+ ```
848
+
849
+ - `command` — The whole-suite run's command.
850
+ - `locations` — The failing `file:line`s to re-run, as the run's own log printed them — or, for a run that started outside the suite, the absolute form Playwright matches each file argument against.
851
+
852
+ **Returns:** The scoped command, or the command unchanged when it runs no Playwright suite.
853
+
854
+ #### `suiteRunInTheWay(candidate, inFlight)`
855
+
856
+ The run in flight that makes a second run a hazard, or null when nothing is in the way.
857
+
858
+ This is the ONE rule the mid-run note and the suite-overlap hold both answer with: a suite run
859
+ may start only when this returns null, because two suites against one app and database share its
860
+ rows — the failures one causes the other cannot be told from real ones. A run that invokes no
861
+ suite is never in the way, and neither is one the candidate command stops first.
862
+
863
+ ```typescript
864
+ function suiteRunInTheWay(candidate: { target: string }, inFlight: readonly T[]): T | null
865
+ ```
866
+
867
+ - `candidate` — The run about to start, by the app and database it would measure ({@link suiteCommandTarget}).
868
+ - `inFlight` — The suite runs already going, in the order the caller found them.
869
+
870
+ **Returns:** The first run in the way, or null.
871
+
872
+ #### `isDiagnosisRun(command)`
873
+
874
+ Whether a command walks a PART of a suite instead of all of it: a cap that stops it at a failure
875
+ count, the failures the last run printed (`--last-failed`, `--only-changed`), a title filter, or
876
+ named files/lines.
877
+
878
+ THE one reading of that question, in one place. Two sites act on the answer and must never
879
+ disagree:
880
+
881
+ - the command rewriter that adds `--retries=0` to a background Playwright run
882
+ ({@link ./utilities.js} `streamingBackgroundCommand`) — a diagnosis walks each failure once,
883
+ while the run the suite is judged by keeps the suite's own retries; and
884
+ - the platform's own notes (molecule-dev's `suite-cli-override.ts`), which must not tell the
885
+ executor to drop the `--retries=0` the platform itself has just added — the contradiction X1
886
+ flash-m74 and m75 filed, and m76 filed a third time.
887
+
888
+ Until xr-molecule-flash-m89 they were a regex and a predicate that only LOOKED alike, and the
889
+ gap was exactly the run the platform had asked for: its `testRunNote` says "run the whole suite
890
+ once with `--max-failures=0`", and the rewriter read that cap flag as a capped diagnosis and
891
+ stripped the suite's retries from the uncapped proof run that follows a deploy — the one run the
892
+ graded run is compared against (+25:03, +31:39, +36:40 and +49:57, the last two against the
893
+ deployed URL, where a network blip is likeliest and the retries it dropped are the ones that
894
+ absorb it).
895
+
896
+ `--max-failures=0` is Playwright's own "no cap" ({@link boundsFailures}), so it is not a
897
+
898
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/suite-overlap.d.ts`.)_
899
+
900
+ ```typescript
901
+ function isDiagnosisRun(command: string): boolean
902
+ ```
903
+
904
+ - `command` — The command as the executor sent it.
905
+
906
+ **Returns:** True when the run is a diagnosis or a re-run, not the whole suite.
907
+
609
908
  #### `discoverSkills(backend)`
610
909
 
611
910
  Discover skills from a project directory.
@@ -683,6 +982,36 @@ function moleculeDistReadNote(path: string): string | null
683
982
 
684
983
  **Returns:** The note, or null when the path is not inside a package's `dist`.
685
984
 
985
+ #### `installedPackageWriteRefusal(path)`
986
+
987
+ The refusal for a write whose target is inside an installed package, or null
988
+ when the path is not under a `node_modules` directory.
989
+
990
+ `node_modules` is not the project's code: nothing written there survives the
991
+ next install, no deploy carries it, and a sandbox whose dependency tree is
992
+ shared (the pre-installed superset) breaks every other run through it. X0
993
+ xr-molecule-flash-m91 (+04:43) called `write_file` on
994
+ `node_modules/@molecule/api-resource-comment/dist/numbers.d.ts` while reading
995
+ the package's declarations and clobbered its runtime module a call later
996
+ (`module.exports = {}` over the ESM handler its own code imports) — the write
997
+ answered `ok: true`, and 18 s went to noticing, `npm pack`-ing the package and
998
+ putting the file back. Reading these files stays allowed (that is how the
999
+ declarations above are read at all).
1000
+
1001
+ The restore route is named because the model that has ALREADY clobbered a file
1002
+ (this refusal is too late for it) is the one who needs it, and because the
1003
+ obvious repair — `npm install <pkg>` — is itself refused in the pre-installed
1004
+ sandbox. `npm pack` + a `tar` extraction over the package directory is what
1005
+ m91 used, and what both guards leave alone.
1006
+
1007
+ ```typescript
1008
+ function installedPackageWriteRefusal(path: string): string | null
1009
+ ```
1010
+
1011
+ - `path` — An absolute path a tool is about to write.
1012
+
1013
+ **Returns:** The one-line refusal naming the package, or null when nothing is installed there.
1014
+
686
1015
  #### `buildTools(backend, config)`
687
1016
 
688
1017
  Build a complete set of AI agent tools bound to an execution backend.
@@ -1044,6 +1373,105 @@ function findPatternArgs(pattern: string): string
1044
1373
 
1045
1374
  **Returns:** A `find` predicate: `-name '…'` for a bare name, else one or more `-path '…'` alternatives, ORed inside parens when there is more than one.
1046
1375
 
1376
+ #### `findPathArgs(pattern)`
1377
+
1378
+ The `-path` alternatives of {@link findPatternArgs}, for a pattern whose
1379
+ directory the caller has already rooted the search at.
1380
+
1381
+ `find_files` narrows the search root itself when the pattern spells its own
1382
+ directory out (see {@link literalDirPrefix}) — `node_modules` plus
1383
+ `@molecule/api-resource-user/dist/*.d.ts` searched that one directory. The
1384
+ remainder (`*.d.ts`) must then keep the `-path` form rather than collapsing to
1385
+ `-name`: `*` crosses `/` in an fnmatch glob, so the `-path` form still finds a
1386
+ declaration file in a `dist/utilities/` subdirectory, which `-name '*.d.ts'`
1387
+ would drop (a silent partial answer, which is worse than the timeout it
1388
+ replaced).
1389
+
1390
+ ```typescript
1391
+ function findPathArgs(pattern: string): string
1392
+ ```
1393
+
1394
+ - `pattern` — The pattern's remainder, with a `/` in it or not.
1395
+
1396
+ **Returns:** One or more `-path '…'` alternatives, ORed inside parens when there is more than one.
1397
+
1398
+ #### `literalDirPrefix(pattern)`
1399
+
1400
+ The directory a GLOB pattern spells out, when it spells one out: everything
1401
+ before the pattern's first glob character, trimmed back to the last `/`.
1402
+
1403
+ A pattern is matched against the whole path with a leading "any parent"
1404
+ anchor ({@link findPatternArgs}), so `find` visits every entry below the
1405
+ search root. Handed `/workspace/node_modules` and
1406
+ `@molecule/api-resource-user/dist/*.d.ts` — a directory named outright —
1407
+ `find` walked the sandbox's entire dependency tree and hit the tool's 10 s
1408
+ deadline without printing a result (xr-molecule-flash-m94: the one directory
1409
+ the model meant answered a `list_files` in 0 s). The literal prefix is that
1410
+ directory, and rooting the search at it is what keeps the call instant.
1411
+
1412
+ A BARE first segment (`helpers/*`, `bonds/*`, `e2e/*.spec.ts`) is NOT one:
1413
+ `helpers/*` means "a `helpers/` directory anywhere below the root", which the
1414
+ tool documents and a live run relied on (xr-molecule-flash-m77 — a path
1415
+ pattern is not anchored to one depth). Rooting those at
1416
+ `<root>/helpers` would answer with a SUBSET, silently: the nested
1417
+ `api/src/helpers/` matches would disappear, and a missing file reads as
1418
+ "no such file". So a prefix is returned only when it names a path of two or
1419
+ more segments (`@molecule/api-resource-user/dist`), which is the shape that
1420
+ is a directory rather than a directory name.
1421
+
1422
+ ```typescript
1423
+ function literalDirPrefix(pattern: string): string | null
1424
+ ```
1425
+
1426
+ - `pattern` — A pattern already accepted by {@link isValidGlob}.
1427
+
1428
+ **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.
1429
+
1430
+ #### `isCommandTimeout(error)`
1431
+
1432
+ Whether a backend error is the sandbox's own command deadline.
1433
+
1434
+ The E2B SDK answers a timed-out `exec` with its generic text —
1435
+ `[deadline_exceeded] the operation timed out: … It can be modified by passing
1436
+ 'timeoutMs' when making the request. Use '0' to disable the timeout.` — which
1437
+ names an option the executor cannot set and says nothing about the search
1438
+ that died. Handing that sentence to the model is what xr-molecule-flash-m94
1439
+ read after `find_files` spent 10 s walking `node_modules`; the tool answers
1440
+ the deadline in its own words instead (see {@link searchTimeoutError}).
1441
+
1442
+ ```typescript
1443
+ function isCommandTimeout(error: unknown): boolean
1444
+ ```
1445
+
1446
+ - `error` — The error the backend threw.
1447
+
1448
+ **Returns:** True when the error reads as a timeout or a deadline.
1449
+
1450
+ #### `searchTimeoutError(tool, searched, pattern, seconds, hint)`
1451
+
1452
+ The answer a search tool gives when the backend's deadline fires: what was
1453
+ searched, for what, how long the tool allowed, and the two ways to get an
1454
+ answer — a narrower `path`, or a listing. Never the vendor's `timeoutMs`
1455
+ sentence (see {@link isCommandTimeout}).
1456
+
1457
+ ```typescript
1458
+ function searchTimeoutError(
1459
+ tool: string,
1460
+ searched: string,
1461
+ pattern: string,
1462
+ seconds: number,
1463
+ hint: string,
1464
+ ): string
1465
+ ```
1466
+
1467
+ - `tool` — The tool that timed out (`find_files`, `search_files`).
1468
+ - `searched` — The directory the search actually covered.
1469
+ - `pattern` — The pattern it was searching for.
1470
+ - `seconds` — The tool's own limit, in seconds.
1471
+ - `hint` — What to do differently — the narrower target or the listing tool.
1472
+
1473
+ **Returns:** The message's text.
1474
+
1047
1475
  #### `pathArgError(path, tool)`
1048
1476
 
1049
1477
  Validate a file tool's `path` argument is a non-empty string. A weak model
@@ -1364,6 +1792,31 @@ function failureCausesNote(log: string): string | null
1364
1792
 
1365
1793
  **Returns:** The note, or null.
1366
1794
 
1795
+ #### `rerunOfRunningSuite(run, locations)`
1796
+
1797
+ The command that re-runs the tests at `locations` the way the run IN FLIGHT ran them: the same
1798
+ invocation, in the same directory, narrowed to those tests — or null when the running command
1799
+ is not a Playwright run this can narrow.
1800
+
1801
+ The `locations` are the paths the run's own log printed, which are relative to the directory
1802
+ that run started in, so a re-run has to resolve them there: the command names that directory
1803
+ unless it already changes into one of its own, because the executor's next command starts
1804
+ wherever it starts. The returned command is for AFTER the run ends — it is a second run against
1805
+ the same app and database, and that is exactly what is refused while the run walks
1806
+ ({@link suiteRunInTheWay}).
1807
+
1808
+ ```typescript
1809
+ function rerunOfRunningSuite(
1810
+ run: { command: string; cwd?: string },
1811
+ locations: readonly string[],
1812
+ ): string | null
1813
+ ```
1814
+
1815
+ - `run` — The run in flight: the command it was started with, as it was run, and the directory that command ran in.
1816
+ - `locations` — The failing `file:line`s to re-run, as the run's log printed them.
1817
+
1818
+ **Returns:** The re-run command, or null when there is no suite to narrow.
1819
+
1367
1820
  #### `repeatingFailureNote(found, o)`
1368
1821
 
1369
1822
  The sentence that names a cause repeating among a running suite's failures, mid-run.
@@ -1379,15 +1832,30 @@ cause was certain at the third, 47 s in, and the note answers exactly that; X1 f
1379
1832
  stopped eight runs at their first repeating cause in 14.5 minutes and never finished one, so
1380
1833
  the note must not say to stop when the run still has other causes to name.
1381
1834
 
1835
+ The run is STILL WALKING while this note is written — both callers produce it mid-run — so a
1836
+ re-run of one of its tests is a second run against the same app and database, and the platform
1837
+ refuses that ({@link suiteRunInTheWay}, the same rule its overlap hold answers with):
1838
+ xr-molecule-flash-m89 (2026-10-16) read _"Start on them now, while the run keeps walking.
1839
+ `null-fields.spec.ts:53` alone reproduces the first one."_ and sent exactly that re-run 23 s
1840
+ later, where the hold answered *"A playwright suite is already running against this app and
1841
+
1842
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/utilities.d.ts`.)_
1843
+
1382
1844
  ```typescript
1383
1845
  function repeatingFailureNote(
1384
1846
  found: RepeatingFailure,
1385
- o: { taskId: string; log: string; stop: string; universal?: UniversalCause | null },
1847
+ o: {
1848
+ taskId: string
1849
+ log: string
1850
+ stop: string
1851
+ universal?: UniversalCause | null
1852
+ run?: { command: string; cwd?: string }
1853
+ },
1386
1854
  ): string
1387
1855
  ```
1388
1856
 
1389
1857
  - `found` — What {@link repeatingFailure} found.
1390
- - `o` — The run's facts: its `taskId`, its `log` path, `stop` — the shell command that stops it, printed only for a {@link UniversalCause} — and `universal`, what {@link universalCause} found (absent means the run is not failing whole).
1858
+ - `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.
1391
1859
 
1392
1860
  **Returns:** The note.
1393
1861
 
@@ -1469,6 +1937,15 @@ function fusedLine(content: string, newString: string): { line: number; text: st
1469
1937
 
1470
1938
  ### Constants
1471
1939
 
1940
+ #### `CUT_AT_BUDGET`
1941
+
1942
+ The state a result carries when a budget stopped its command: a run that did not finish is not a
1943
+ run that passed, and no reader may treat it as one.
1944
+
1945
+ ```typescript
1946
+ const CUT_AT_BUDGET: 'cut_at_budget'
1947
+ ```
1948
+
1472
1949
  #### `TOOL_SCHEMAS`
1473
1950
 
1474
1951
  Canonical tool schemas shared by the agent runtime and documentation.
@@ -1477,6 +1954,34 @@ Canonical tool schemas shared by the agent runtime and documentation.
1477
1954
  const TOOL_SCHEMAS: Record<string, ToolSchema>
1478
1955
  ```
1479
1956
 
1957
+ #### `SPLIT_WORK_ADVICE`
1958
+
1959
+ The closing advice on an over-budget message for a command that is not a test
1960
+ suite: the work it was chaining is what to split.
1961
+
1962
+ ```typescript
1963
+ const SPLIT_WORK_ADVICE: string
1964
+ ```
1965
+
1966
+ #### `SUITE_OVER_BUDGET_ADVICE`
1967
+
1968
+ The closing advice on an over-budget message for a command that IS a suite run
1969
+ (Playwright, Vitest, a `hurl` run — {@link isSuiteRunCommand}): its files are not
1970
+ a bigger unit to break up.
1971
+
1972
+ X1 flash-m36 asked for this half and the message kept saying "run a smaller unit"
1973
+ for two months. It is the wrong direction for a suite, and measurably: a run
1974
+ stopped here reports NO tally, so nothing in the result says which test or request
1975
+ hung — the run has to be sent again in the background (off this budget, its output
1976
+ written to a log that can be read mid-run) or, when a consumer knows the runner's
1977
+ per-request limit, with each request bounded. xr-molecule-flash-m93 (+06:22,
1978
+ 2026-10-16) lost 4.8 minutes to a Hurl run whose ONE unanswered request had hurl's
1979
+ 300 s default per request holding the file, ten seconds past this tool's budget.
1980
+
1981
+ ```typescript
1982
+ const SUITE_OVER_BUDGET_ADVICE: string
1983
+ ```
1984
+
1480
1985
  #### `MAX_READ_SIZE`
1481
1986
 
1482
1987
  Max file size for read_file (5MB).
@@ -1800,6 +2305,18 @@ Peer dependencies:
1800
2305
  `newlineNote` hint beside an unrelated-looking parse error, which an executor
1801
2306
  repairs by rewriting the fused line's text and fuses the next one. The
1802
2307
  `newlineNote` alone is still returned when the file parses.
2308
+ - **A replacement that only changes a line break at its boundary is refused,
2309
+ not applied.** When `new_string` is `old_string` with its trailing newline
2310
+ added or removed and nothing else, the edit can only move text across lines —
2311
+ joining the next line onto the replaced text, or pushing the rest of the line
2312
+ down — which is what a weak model does by accident when it repeats a line it
2313
+ means to keep (flash-m28, m55, m62, m89, five times in the m89 run alone). The
2314
+ replacement is skipped, the rest of the batch is applied, and the result
2315
+ carries `skippedNote`: one line naming the replacement, what applying it would
2316
+ have done, and how to say it on purpose (include the next line in `old_string`).
2317
+ The file's own next character decides: text on the same line is a join or a
2318
+ split and is refused; a blank line after it, or the end of the file, means no
2319
+ text crosses lines and the edit is applied as written.
1803
2320
  - **A parameter under another name is not a broken tool.** Every tool's input
1804
2321
  is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
1805
2322
  snake_case), and a handler that throws returns an error naming the missing
@@ -22,7 +22,7 @@
22
22
  * (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
23
23
  * `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
24
24
  * WRITES is the caller's business, so a command with a heredoc goes through the caller's own
25
- * stripper first (`withoutHeredocs` in molecule-dev's suite checks).
25
+ * stripper first (`withoutHeredocs`, in `./suite-overlap.js`).
26
26
  *
27
27
  * @module
28
28
  */
@@ -37,7 +37,19 @@ export interface CommandSegment {
37
37
  args: readonly string[];
38
38
  /** The program and its arguments as one line — what a runner pattern is matched against. */
39
39
  text: string;
40
+ /**
41
+ * The seconds the segment's own `timeout` wrapper allows the command it runs
42
+ * (`timeout 280 hurl --test …` → 280), or null when it carries none.
43
+ */
44
+ timeoutSeconds: number | null;
40
45
  }
46
+ /**
47
+ * The seconds a duration word stands for.
48
+ *
49
+ * @param word - A duration (`280`, `20s`, `1m`).
50
+ * @returns The seconds, or null when the word is not a duration.
51
+ */
52
+ export declare function durationSeconds(word: string): number | null;
41
53
  /**
42
54
  * The segments of a command, each with its program and arguments — the answer every classifier
43
55
  * that reads a command as "this runs the suite" is asking for.
@@ -46,6 +58,20 @@ export interface CommandSegment {
46
58
  * @returns One entry per statement or pipeline stage that runs something, in order.
47
59
  */
48
60
  export declare function commandSegments(command: string): CommandSegment[];
61
+ /**
62
+ * The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
63
+ * under (`timeout 280 hurl --test …` → 280), or null when it carries none.
64
+ *
65
+ * This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
66
+ * finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
67
+ * other command reader uses, rather than matched as text: `timeout` in an argument
68
+ * (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
69
+ * the bound ({@link WRAPPER_VALUE_FLAGS}).
70
+ *
71
+ * @param command - The shell command.
72
+ * @returns The seconds the command allows itself, or null.
73
+ */
74
+ export declare function commandTimeoutSeconds(command: string): number | null;
49
75
  /**
50
76
  * Whether any segment of the command runs a program whose command-position text matches
51
77
  * `pattern` — the one question a runner-kind table asks.
@@ -1 +1 @@
1
- {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;CACb;AAqRD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAEjE;AA+BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
1
+ {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAmFD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAyPD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAEjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAoCD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}