@molecule/api-ai-tools 1.10.3 → 1.10.4

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-19T03:15:38.119Z
6
+ Generated: 2026-10-19T06:38:37.559Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -216,6 +216,12 @@ interface CutRun {
216
216
  noun: 'files' | 'tests'
217
217
  /** The last thing the run printed — the line a reader looks at first. */
218
218
  doing: string | null
219
+ /**
220
+ * The interpreter of an inline program that only touches local files (`python3`, `node`, …), or
221
+ * null. Such a program is stopped by its short budget because it is stuck, never because the work
222
+ * is slow, so the sentence must not send it to the background (xr-molecule-flash-m200 +03:45).
223
+ */
224
+ inlineScript?: string | null
219
225
  }
220
226
  ```
221
227
 
@@ -1070,6 +1076,17 @@ interface ToolBuildConfig {
1070
1076
  */
1071
1077
  suiteBudget?: (command: string, cwd: string) => Promise<SuiteBudgetPin | null | undefined>
1072
1078
 
1079
+ /**
1080
+ * A whole walk's duration in ms, as the consumer measured it. A suite run the executor detached
1081
+ * whose own `timeout` is shorter than this is lifted (m200); unset lifts only a bound no longer
1082
+ * than this tool's ceiling.
1083
+ *
1084
+ * @param command - The suite command.
1085
+ * @param cwd - The run's working directory.
1086
+ * @returns The duration in ms, or null.
1087
+ */
1088
+ suiteRunMs?: (command: string, cwd: string) => Promise<number | null | undefined>
1089
+
1073
1090
  /** Post-write hook (e.g. auto-format via Prettier/ESLint). Called after every write_file/edit_file. */
1074
1091
  onAfterWrite?: (path: string) => Promise<void>
1075
1092
 
@@ -1703,7 +1720,9 @@ Written to be quotable by the model in one sentence and to leave no room for "ne
1703
1720
  the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
1704
1721
  read elsewhere (the suite's own file count) fills in the "of N" it can then state. The sentence
1705
1722
  says how many tests the stop left unrun — a number a reader cannot round up to "the suite
1706
- passed" — and hands over the one move that survives a long run: run it in the background.
1723
+ passed" — and hands over the one move that survives a long run: run it in the background. An
1724
+ inline program that only touches local files is the exception ({@link CutRun.inlineScript}): it
1725
+ is not slow, it is stuck, and the background is the one place it must not be sent.
1707
1726
 
1708
1727
  ```typescript
1709
1728
  function cutRunSentence(run: CutRun): string
@@ -1743,6 +1762,37 @@ function allConnectionFailures(failures: readonly (RunFailureClass | null)[]): b
1743
1762
 
1744
1763
  **Returns:** True when there is at least one and none of them is the app's.
1745
1764
 
1765
+ #### `inlineLocalScriptCommand(command)`
1766
+
1767
+ The interpreter of an INLINE program that only reads and writes local files — the shape that gets
1768
+ {@link INLINE_SCRIPT_BUDGET_MS} instead of the tool's command budget.
1769
+
1770
+ An inline program with any external effect keeps the full budget: no network, install, test
1771
+ runner, build or server verb may appear anywhere in the command ({@link EXTERNAL_EFFECT}), and
1772
+ the command must not be a suite run or a foreground server (the platform's own classifiers).
1773
+
1774
+ ```typescript
1775
+ function inlineLocalScriptCommand(command: string): string | null
1776
+ ```
1777
+
1778
+ - `command` — The command as the executor sent it.
1779
+
1780
+ **Returns:** The interpreter's name (`python3`, `node`, …), or null when this is not the shape.
1781
+
1782
+ #### `inlineScriptRunawaySentence(interpreter, seconds)`
1783
+
1784
+ What a cut inline program's result says instead of the background advice: the work never was
1785
+ slow, it is stuck, and re-running it — here or in the background — cannot help.
1786
+
1787
+ ```typescript
1788
+ function inlineScriptRunawaySentence(interpreter: string, seconds: number): string
1789
+ ```
1790
+
1791
+ - `interpreter` — The interpreter the program ran under (`python3`, `node`, …).
1792
+ - `seconds` — The short budget it was stopped at, in seconds.
1793
+
1794
+ **Returns:** The sentence the result carries.
1795
+
1746
1796
  #### `normalizeToolInput(schema, raw)`
1747
1797
 
1748
1798
  Move known aliases onto the declared parameter names and report what is still
@@ -3086,6 +3136,32 @@ function isValidGlob(pattern: string): boolean
3086
3136
 
3087
3137
  **Returns:** `true` when the pattern contains only allowed characters.
3088
3138
 
3139
+ #### `expandBraceGlob(pattern)`
3140
+
3141
+ Expand ONE level of brace alternatives in a glob pattern, validating every
3142
+ expanded pattern.
3143
+
3144
+ Neither search tool expands braces itself: `grep --include` and `find` take
3145
+ the pattern as a single shell-quoted argument, so no shell ever sees it.
3146
+ `find_files {pattern:"*.{ts,tsx}"}` and `search_files {include:"*.{ts,tsx}"}`
3147
+ were both refused outright (xr-molecule-flash-m199 and m189), costing a round
3148
+ trip and pushing the agent to a narrower search. The group is expanded here
3149
+ instead — `search_files` passes one `--include` per alternative and
3150
+ `find_files` ORs one `-name`/`-path` predicate per alternative.
3151
+
3152
+ Bounded on purpose: one level of braces, alternatives split on `,`, at most
3153
+ {@link MAX_BRACE_GLOB_ALTERNATIVES}, none empty, and each EXPANDED pattern
3154
+ must still pass {@link isValidGlob} — so a shell metacharacter in any
3155
+ alternative (`*.{ts,$(rm)}`) is refused exactly as a bare one is.
3156
+
3157
+ ```typescript
3158
+ function expandBraceGlob(pattern: string): string[] | null
3159
+ ```
3160
+
3161
+ - `pattern` — The user-supplied glob.
3162
+
3163
+ **Returns:** Every pattern the value expands to, or null when it is not accepted (the caller reports its tool's glob error).
3164
+
3089
3165
  #### `findPatternArgs(pattern)`
3090
3166
 
3091
3167
  The `find` predicate for a `find_files` pattern.
@@ -3893,6 +3969,18 @@ run that passed, and no reader may treat it as one.
3893
3969
  const CUT_AT_BUDGET: 'cut_at_budget'
3894
3970
  ```
3895
3971
 
3972
+ #### `INLINE_SCRIPT_BUDGET_MS`
3973
+
3974
+ How long an inline interpreter program that only reads and writes local files may run.
3975
+
3976
+ Such work finishes in milliseconds; if it has not finished in twenty seconds it never will
3977
+ (a backtracking regex on a large file, a loop waiting on something that is not coming), so this
3978
+ is a runaway detector, not a performance budget.
3979
+
3980
+ ```typescript
3981
+ const INLINE_SCRIPT_BUDGET_MS: 20000
3982
+ ```
3983
+
3896
3984
  #### `TOOL_SCHEMAS`
3897
3985
 
3898
3986
  Canonical tool schemas shared by the agent runtime and documentation.
@@ -4085,6 +4173,26 @@ edited `app/src` while the run was still walking — which voids the run.
4085
4173
  const PLAYWRIGHT_FAILURES_INLINE_ENV: 'PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE'
4086
4174
  ```
4087
4175
 
4176
+ #### `MAX_BRACE_GLOB_ALTERNATIVES`
4177
+
4178
+ The most alternatives one brace group may expand to. The point is to accept
4179
+ the ordinary multi-extension form a model writes from habit
4180
+ (`*.{ts,tsx,yml,json,md}`), not to grow a glob engine: a group is bounded so
4181
+ a pattern cannot turn one search into hundreds.
4182
+
4183
+ ```typescript
4184
+ const MAX_BRACE_GLOB_ALTERNATIVES: 16
4185
+ ```
4186
+
4187
+ #### `BRACE_GLOB_HINT`
4188
+
4189
+ The accepted brace form, named in every refusal so a refusal teaches the
4190
+ syntax rather than only reporting that the value was wrong.
4191
+
4192
+ ```typescript
4193
+ const BRACE_GLOB_HINT: string
4194
+ ```
4195
+
4088
4196
  #### `MAX_READ_SIZE`
4089
4197
 
4090
4198
  Max file size for read_file (5MB).
@@ -1 +1 @@
1
- {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;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;AA8RD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAwkBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AAmDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,MAAM,GAAG,IAAI,CAyDf;AA2FD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,GAC1C;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAoD7C;AAED;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;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;AAgSD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAmoBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AAmDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,MAAM,GAAG,IAAI,CAyDf;AA4GD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,GAC1C;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAoD7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
@@ -62,6 +62,7 @@ const LAUNCHERS = new Set([
62
62
  'nice',
63
63
  'nohup',
64
64
  'setsid',
65
+ 'stdbuf',
65
66
  'sudo',
66
67
  'time',
67
68
  'timeout',
@@ -268,6 +269,7 @@ const WRAPPER_VALUE_FLAGS = {
268
269
  env: ['-S', '--split-string', '-u', '--unset', '-C', '--chdir'],
269
270
  ionice: ['-c', '--class', '-n', '--classdata', '-p', '--pid', '-P', '--pgid', '-u', '--uid'],
270
271
  nice: ['-n', '--adjustment'],
272
+ stdbuf: ['-e', '-i', '-o'],
271
273
  sudo: [
272
274
  '-C',
273
275
  '--chdir',
@@ -408,22 +410,40 @@ function nestedCommand(words, from) {
408
410
  return null;
409
411
  }
410
412
  /**
411
- * Where the program starts in one segment's words, and the bound the segment puts on itself.
413
+ * Where the program starts in one segment's words, the bound the segment puts on itself, and the
414
+ * environment it binds for the program it runs.
415
+ *
416
+ * `bound` is every `NAME=value` word the walk steps over between the segment's start and its
417
+ * program: the leading words, and the ones behind an `env` wrapper (`env -i PATH=$PATH
418
+ * BASE_URL=https://… npx playwright test`). A shell passes those to the program in exactly the
419
+ * same way — `env` is the POSIX form of the leading-word form — so a reader that only saw the
420
+ * leading ones called a deployed walk local (X0 xr-molecule-flash-m204 +38:15: two green deployed
421
+ * walks were refused, 5.2 min).
412
422
  *
413
423
  * @param words - The segment's words, quotes kept.
414
424
  * @returns The index of the word in command position (one past the end when the segment runs
415
- * nothing), the command string of a `sh -c '<command>'` this segment turns out to be, and the
416
- * seconds its own `timeout` wrapper allows that command (`timeout 280 hurl …` → 280) or null.
425
+ * nothing), the command string of a `sh -c '<command>'` this segment turns out to be, the
426
+ * seconds its own `timeout` wrapper allows that command (`timeout 280 hurl …` → 280) or null,
427
+ * every `NAME=value` word bound for the program in the order the shell binds them, and the word
428
+ * index an assignment for the program belongs at when an `env` wrapper sits in front of it
429
+ * (`env -i … <here>npx`; a value written before `env -i` is cleared, so it must go after).
417
430
  * `program` names what runs when that is not the word itself's name — `node <path>/playwright`
418
431
  * is a `playwright` invocation, not a `node` one ({@link nodeScriptRunner}).
419
432
  */
420
433
  function commandStart(words) {
421
434
  let at = 0;
422
435
  let timeoutSeconds = null;
436
+ const bound = [];
437
+ let envInsert = null;
438
+ let envReset = false;
423
439
  for (let guard = 0; guard < MAX_WRAPPERS && at < words.length; guard++) {
424
440
  const word = words[at] ?? '';
425
441
  if (ENV_ASSIGNMENT.test(word)) {
442
+ bound.push(word);
426
443
  at += 1;
444
+ // Still the env wrapper's own region: an assignment for the program goes after it.
445
+ if (envInsert !== null)
446
+ envInsert = at;
427
447
  continue;
428
448
  }
429
449
  const name = programName(word);
@@ -435,14 +455,22 @@ function commandStart(words) {
435
455
  if (SHELLS.has(name)) {
436
456
  const nested = nestedCommand(words, at + 1);
437
457
  if (nested !== null)
438
- return { at, nested, timeoutSeconds, program: null };
458
+ return { at, nested, timeoutSeconds, program: null, bound, envInsert, envReset };
439
459
  at += 1 + flagWords(words, at + 1, name);
440
460
  continue;
441
461
  }
442
462
  if (PACKAGE_RUNNERS.has(name)) {
443
463
  const runner = packageRunnerOptions(words, at + 1, name);
444
464
  if (runner.nested !== null)
445
- return { at, nested: runner.nested, timeoutSeconds, program: null };
465
+ return {
466
+ at,
467
+ nested: runner.nested,
468
+ timeoutSeconds,
469
+ program: null,
470
+ bound,
471
+ envInsert,
472
+ envReset,
473
+ };
446
474
  at += 1 + runner.count;
447
475
  continue;
448
476
  }
@@ -451,7 +479,15 @@ function commandStart(words) {
451
479
  const subcommand = unquoted(words[at + 1] ?? '');
452
480
  const runner = packageRunnerOptions(words, at + 2, `${name} ${subcommand}`);
453
481
  if (runner.nested !== null)
454
- return { at, nested: runner.nested, timeoutSeconds, program: null };
482
+ return {
483
+ at,
484
+ nested: runner.nested,
485
+ timeoutSeconds,
486
+ program: null,
487
+ bound,
488
+ envInsert,
489
+ envReset,
490
+ };
455
491
  at += 2 + runner.count;
456
492
  continue;
457
493
  }
@@ -468,18 +504,38 @@ function commandStart(words) {
468
504
  const script = nodeScriptIndex(words, at + 1);
469
505
  const runner = script === null ? null : nodeScriptRunner(words[script] ?? '');
470
506
  return runner === null
471
- ? { at, nested: null, timeoutSeconds, program: null }
472
- : { at: script ?? at, nested: null, timeoutSeconds, program: runner };
507
+ ? { at, nested: null, timeoutSeconds, program: null, bound, envInsert, envReset }
508
+ : {
509
+ at: script ?? at,
510
+ nested: null,
511
+ timeoutSeconds,
512
+ program: runner,
513
+ bound,
514
+ envInsert,
515
+ envReset,
516
+ };
473
517
  }
474
518
  if (!LAUNCHERS.has(name))
475
- return { at, nested: null, timeoutSeconds, program: null };
519
+ return { at, nested: null, timeoutSeconds, program: null, bound, envInsert, envReset };
520
+ const from = at;
476
521
  at += 1 + flagWords(words, at + 1, name);
522
+ // Past `env` and its own options: an assignment for the program written here survives
523
+ // `env -i`, which clears everything before it.
524
+ if (name === 'env') {
525
+ envInsert = at;
526
+ // `env -i` clears the environment it inherited: the words bound before it are NOT the
527
+ // environment the program runs in, so a reader must not attribute them to the run.
528
+ if (words.slice(from + 1, at).some((flag) => flag === '-i' || flag === '--ignore-environment'))
529
+ envReset = true;
530
+ }
531
+ if (envReset)
532
+ bound.length = 0;
477
533
  if (name === 'timeout' && DURATION.test(words[at] ?? '')) {
478
534
  timeoutSeconds = durationSeconds(words[at] ?? '');
479
535
  at += 1;
480
536
  }
481
537
  }
482
- return { at, nested: null, timeoutSeconds, program: null };
538
+ return { at, nested: null, timeoutSeconds, program: null, bound, envInsert, envReset };
483
539
  }
484
540
  /**
485
541
  * A command with every heredoc BODY blanked to spaces, so its length and every offset in it are
@@ -1073,11 +1129,24 @@ function locatedSegmentsOf(command, depth) {
1073
1129
  const words = segmentWords(part.text);
1074
1130
  const start = commandStart(words);
1075
1131
  const leading = leadingAssignments(part.text);
1132
+ // Every `NAME=value` bound for the program the statement runs: its leading words AND the ones
1133
+ // an `env` wrapper carries (`env -i PATH=$PATH BASE_URL=… npx playwright test`). A shell binds
1134
+ // both to the program the same way, so one list holds both — a reader that saw only the
1135
+ // leading ones read a deployed walk as local (X0 xr-molecule-flash-m204 +38:15). `env -i`
1136
+ // clears what it inherited, so after it the wrapper's own words are the whole environment.
1137
+ const assignments = [
1138
+ ...new Set(start.envReset ? start.bound : [...leading.words, ...start.bound]),
1139
+ ];
1076
1140
  /** Every offset in this statement is an offset in the command it was written in. */
1077
1141
  const placed = {
1078
1142
  start: part.start,
1079
1143
  end: part.start + part.text.length,
1080
- assignmentOffset: part.start + leading.end,
1144
+ // After the statement's own leading `NAME=value` words — or, when an `env` wrapper stands
1145
+ // there, after that wrapper and its own options/assignments, because `env -i` clears what
1146
+ // comes before it (`env -i … <here>npx playwright test`).
1147
+ assignmentOffset: start.envInsert === null
1148
+ ? part.start + leading.end
1149
+ : part.start + (wordOffset(part.text, start.envInsert) ?? leading.end),
1081
1150
  programOffset: words[start.at] === undefined ? null : part.start + (wordOffset(part.text, start.at) ?? 0),
1082
1151
  };
1083
1152
  // The runner the word RUNS names, when that is not the word's own name (`node <path>/playwright`
@@ -1093,6 +1162,9 @@ function locatedSegmentsOf(command, depth) {
1093
1162
  continue;
1094
1163
  out.push({
1095
1164
  ...entry,
1165
+ // The variables this statement bound before the shell reached it are the nested
1166
+ // command's too: `env BASE_URL=… sh -c '… npx playwright test …'` passes them down.
1167
+ assignments: [...new Set([...assignments, ...entry.assignments])],
1096
1168
  // A nested statement's positions are positions in the nested string, not in this
1097
1169
  // command. The FIRST one keeps this statement's own separator: the shell reads the
1098
1170
  // nested string where this statement stands.
@@ -1115,10 +1187,10 @@ function locatedSegmentsOf(command, depth) {
1115
1187
  if (word === undefined) {
1116
1188
  // Only wrappers and assignments: nothing this statement runs, but the variables it sets
1117
1189
  // persist in the shell the rest of the command runs in.
1118
- if (leading.words.length > 0)
1190
+ if (assignments.length > 0)
1119
1191
  out.push({
1120
1192
  segment: null,
1121
- assignments: leading.words,
1193
+ assignments,
1122
1194
  ...placed,
1123
1195
  programOffset: null,
1124
1196
  separator: part.before,
@@ -1134,7 +1206,7 @@ function locatedSegmentsOf(command, depth) {
1134
1206
  text: [program, ...args].join(' '),
1135
1207
  timeoutSeconds: start.timeoutSeconds,
1136
1208
  },
1137
- assignments: leading.words,
1209
+ assignments,
1138
1210
  ...placed,
1139
1211
  separator: part.before,
1140
1212
  terminator: part.after,
package/dist/cut-run.d.ts CHANGED
@@ -161,6 +161,12 @@ export interface CutRun {
161
161
  noun: 'files' | 'tests';
162
162
  /** The last thing the run printed — the line a reader looks at first. */
163
163
  doing: string | null;
164
+ /**
165
+ * The interpreter of an inline program that only touches local files (`python3`, `node`, …), or
166
+ * null. Such a program is stopped by its short budget because it is stuck, never because the work
167
+ * is slow, so the sentence must not send it to the background (xr-molecule-flash-m200 +03:45).
168
+ */
169
+ inlineScript?: string | null;
164
170
  }
165
171
  /**
166
172
  * The run a command's result records as stopped by a budget, or null when the result is a run that
@@ -213,7 +219,9 @@ export declare function cutRunFacts(result: unknown): CutRun | null;
213
219
  * the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
214
220
  * read elsewhere (the suite's own file count) fills in the "of N" it can then state. The sentence
215
221
  * says how many tests the stop left unrun — a number a reader cannot round up to "the suite
216
- * passed" — and hands over the one move that survives a long run: run it in the background.
222
+ * passed" — and hands over the one move that survives a long run: run it in the background. An
223
+ * inline program that only touches local files is the exception ({@link CutRun.inlineScript}): it
224
+ * is not slow, it is stuck, and the background is the one place it must not be sent.
217
225
  *
218
226
  * @param run - The stopped run.
219
227
  * @returns The sentence.
@@ -1 +1 @@
1
- {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAuFf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAiB1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiClD"}
1
+ {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CA4Ff;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkB1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqClD"}
package/dist/cut-run.js CHANGED
@@ -33,6 +33,7 @@
33
33
  * @module
34
34
  */
35
35
  import { commandTimeoutSeconds } from './command-position.js';
36
+ import { inlineLocalScriptCommand } from './inline-script.js';
36
37
  import { isSuiteRunCommand } from './utilities.js';
37
38
  /**
38
39
  * The state a result carries when a budget stopped its command: a run that did not finish is not a
@@ -287,6 +288,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
287
288
  if (summary?.finished)
288
289
  return null;
289
290
  const suite = isSuiteRunCommand(command);
291
+ const inlineScript = inlineLocalScriptCommand(command);
290
292
  const withheld = /\|\s*(?:tail|head|grep|egrep|fgrep|sort|uniq|wc|jq)\b/.test(command);
291
293
  const progress = runProgress(detected, suite, summary);
292
294
  const doing = lastPrinted(text);
@@ -315,6 +317,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
315
317
  exitCode,
316
318
  ...progress,
317
319
  doing,
320
+ inlineScript,
318
321
  };
319
322
  }
320
323
  if (declared !== null && ownBoundStops && elapsedMs >= atBoundMs(declared)) {
@@ -326,6 +329,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
326
329
  exitCode: withheld || exitCode === 0 ? null : exitCode,
327
330
  ...progress,
328
331
  doing,
332
+ inlineScript,
329
333
  };
330
334
  }
331
335
  // The tool's budget with its code masked: the run used the whole budget and the pipeline left
@@ -338,6 +342,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
338
342
  exitCode: exitCode === 0 ? null : exitCode,
339
343
  ...progress,
340
344
  doing,
345
+ inlineScript,
341
346
  };
342
347
  }
343
348
  // The runner printed a summary that does not account for the total it announced, and neither
@@ -354,6 +359,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
354
359
  exitCode: withheld || exitCode === 0 ? null : exitCode,
355
360
  ...progress,
356
361
  doing,
362
+ inlineScript,
357
363
  };
358
364
  }
359
365
  return null;
@@ -390,6 +396,7 @@ export function cutRunFacts(result) {
390
396
  total: typeof c.total === 'number' ? c.total : null,
391
397
  noun: c.noun === 'tests' ? 'tests' : 'files',
392
398
  doing: typeof c.doing === 'string' && c.doing ? c.doing : null,
399
+ inlineScript: typeof c.inlineScript === 'string' && c.inlineScript ? c.inlineScript : null,
393
400
  };
394
401
  }
395
402
  /**
@@ -400,7 +407,9 @@ export function cutRunFacts(result) {
400
407
  * the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
401
408
  * read elsewhere (the suite's own file count) fills in the "of N" it can then state. The sentence
402
409
  * says how many tests the stop left unrun — a number a reader cannot round up to "the suite
403
- * passed" — and hands over the one move that survives a long run: run it in the background.
410
+ * passed" — and hands over the one move that survives a long run: run it in the background. An
411
+ * inline program that only touches local files is the exception ({@link CutRun.inlineScript}): it
412
+ * is not slow, it is stuck, and the background is the one place it must not be sent.
404
413
  *
405
414
  * @param run - The stopped run.
406
415
  * @returns The sentence.
@@ -417,11 +426,15 @@ export function cutRunSentence(run) {
417
426
  // from being sent to the background again — when its own `timeout` is what stopped it, that
418
427
  // prefix is the thing to drop (X1 xr-molecule-flash-m143 +24:45: three cut sentences in one run
419
428
  // told a background walk to "run it in the background", which is where it already was).
420
- const background = run.background
421
- ? run.stoppedBy === 'command-timeout'
422
- ? `drop the \`timeout\` prefix and send it again — in the background a run is bounded by \`wait_for_task\` and the turn's deadline, not by this tool's ceiling, so the bound you wrote is what killed it`
423
- : 'send it again when you are ready'
424
- : 'run it in the background (run_in_background + wait_for_task)';
429
+ const background = run.inlineScript
430
+ ? // An inline local-file program finished in milliseconds or is stuck; backgrounding it only
431
+ // moves the hang (xr-molecule-flash-m200 +03:45). The full rewrite advice rides the error.
432
+ 'do not run it in the background — the transform is stuck, not slow'
433
+ : run.background
434
+ ? run.stoppedBy === 'command-timeout'
435
+ ? `drop the \`timeout\` prefix and send it again — in the background a run is bounded by \`wait_for_task\` and the turn's deadline, not by this tool's ceiling, so the bound you wrote is what killed it`
436
+ : 'send it again when you are ready'
437
+ : 'run it in the background (run_in_background + wait_for_task)';
425
438
  const missing = run.ran !== null && run.total !== null ? run.total - run.ran : null;
426
439
  const lead = missing !== null
427
440
  ? `Ran ${run.ran} of ${run.total} ${run.noun} in ${run.seconds} s and was stopped by ${by} — the ${missing} it never reached ${missing === 1 ? 'has' : 'have'} not run; ${background}`
package/dist/index.d.ts CHANGED
@@ -135,6 +135,7 @@ export * from './browser-guard.js';
135
135
  export * from './command-position.js';
136
136
  export * from './cut-run.js';
137
137
  export * from './failure-kind.js';
138
+ export * from './inline-script.js';
138
139
  export * from './input-normalizer.js';
139
140
  export * from './line-diff.js';
140
141
  export * from './no-test-run.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -135,6 +135,7 @@ export * from './browser-guard.js';
135
135
  export * from './command-position.js';
136
136
  export * from './cut-run.js';
137
137
  export * from './failure-kind.js';
138
+ export * from './inline-script.js';
138
139
  export * from './input-normalizer.js';
139
140
  export * from './line-diff.js';
140
141
  export * from './no-test-run.js';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * An inline interpreter program (`python3 -c`, `python3 - <<EOF`, `node -e`) that only touches local
3
+ * files — work that takes milliseconds or never ends, never minutes of honest progress.
4
+ *
5
+ * xr-molecule-flash-m200 (+03:45, 2026-10-19) spent 291 s of a 60-minute cap on
6
+ * `python3 - <<'EOF' … re.findall(r'([^{}]*:before[^{}]*)\{content:"[^"]*"\}', s) … EOF` rewriting a
7
+ * minified CSS file whose first rule is one ~25 KB selector list: the pattern's two unbounded
8
+ * `[^{}]*` around a literal backtrack cubically, printed nothing, and was stopped by the tool's
9
+ * full 290 s budget. The step was also unnecessary. Nothing in the tool distinguished that program
10
+ * — a local text transform that either finishes at once or is stuck — from a build that is genuinely
11
+ * slow, so it was given a build's budget and a build's advice ("run it in the background"), which
12
+ * only turns a hang into a longer hang.
13
+ *
14
+ * So an inline interpreter program with no network, install, test-runner, build or server verb gets
15
+ * a short budget ({@link INLINE_SCRIPT_BUDGET_MS}), and a cut says what happened: a runaway
16
+ * transform, not slow work ({@link inlineScriptRunawaySentence}).
17
+ *
18
+ * @module
19
+ */
20
+ /**
21
+ * How long an inline interpreter program that only reads and writes local files may run.
22
+ *
23
+ * Such work finishes in milliseconds; if it has not finished in twenty seconds it never will
24
+ * (a backtracking regex on a large file, a loop waiting on something that is not coming), so this
25
+ * is a runaway detector, not a performance budget.
26
+ */
27
+ export declare const INLINE_SCRIPT_BUDGET_MS = 20000;
28
+ /**
29
+ * The interpreter of an INLINE program that only reads and writes local files — the shape that gets
30
+ * {@link INLINE_SCRIPT_BUDGET_MS} instead of the tool's command budget.
31
+ *
32
+ * An inline program with any external effect keeps the full budget: no network, install, test
33
+ * runner, build or server verb may appear anywhere in the command ({@link EXTERNAL_EFFECT}), and
34
+ * the command must not be a suite run or a foreground server (the platform's own classifiers).
35
+ *
36
+ * @param command - The command as the executor sent it.
37
+ * @returns The interpreter's name (`python3`, `node`, …), or null when this is not the shape.
38
+ */
39
+ export declare function inlineLocalScriptCommand(command: string): string | null;
40
+ /**
41
+ * What a cut inline program's result says instead of the background advice: the work never was
42
+ * slow, it is stuck, and re-running it — here or in the background — cannot help.
43
+ *
44
+ * @param interpreter - The interpreter the program ran under (`python3`, `node`, …).
45
+ * @param seconds - The short budget it was stopped at, in seconds.
46
+ * @returns The sentence the result carries.
47
+ */
48
+ export declare function inlineScriptRunawaySentence(interpreter: string, seconds: number): string;
49
+ //# sourceMappingURL=inline-script.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-script.d.ts","sourceRoot":"","sources":["../src/inline-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAsD7C;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASvE;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOxF"}