@molecule/api-ai-tools 1.5.15 → 1.5.16

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-16T05:16:44.196Z
6
+ Generated: 2026-10-16T07:29:43.225Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -572,6 +572,22 @@ function redactSecretsInCode(s: string): string
572
572
 
573
573
  **Returns:** A redacted copy that preserves ordinary code verbatim.
574
574
 
575
+ #### `printsOnlySource(command)`
576
+
577
+ Whether a shell command only PRINTS source files (`cat a.tsx`, `sed -n '1,40p' x.ts`,
578
+ `head -50 b.hurl`): its output is file content and gets the code-safe redaction, like
579
+ read_file. Through the env-dump grade a printed `type AuthState = 'authenticated' | …`
580
+ came back as `'[REDACTED]'`, and the executor wrote the mask into a new file (X1
581
+ flash-m58). Any segment that is not such a print, or any env file named, keeps the full grade.
582
+
583
+ ```typescript
584
+ function printsOnlySource(command: string): boolean
585
+ ```
586
+
587
+ - `command` — The shell command.
588
+
589
+ **Returns:** True when every segment prints source files only.
590
+
575
591
  #### `isEnvFilePath(path)`
576
592
 
577
593
  Whether a path is an env file, for which {@link redactSecrets}' full env-dump
@@ -646,9 +662,16 @@ killed (X1 run flash-m15 lost 20 of its 60 minutes this way, 2026-10-15):
646
662
  - a trailing `| tail -N`, which holds everything back until the command ends.
647
663
  It is dropped: `wait_for_task` already returns the end of the log.
648
664
  - `playwright test` with no `--reporter`, which runs the config's reporter —
649
- often `html`, which prints nothing per test. `--reporter=line` is added
665
+ often `html`, which prints nothing per test. `--reporter=list` is added
650
666
  when the playwright run is the command's last step and nothing is piped or
651
- redirected from it ( aside); the results are the same, printed as they happen.
667
+ redirected from it: it writes each result as it finishes, and it is the
668
+ shape the platform reads a background suite back in (`suite-fail-fast.ts`'s
669
+ fail-fast note and the Tests bar both parse `list` lines). A run that names
670
+ `--reporter=line` is switched to `list` for the same reason — `line` writes
671
+ `[n/N] <title>` with no ✓/✘ mark and no duration, so on X1 flash-m62 an
672
+ 8.3-minute walk of one suite could never trip the fail-fast note.
673
+
674
+ Only the command's last step is touched, and only its reporter.
652
675
 
653
676
  ```typescript
654
677
  function streamingBackgroundCommand(command: string): { command: string; changes: string[] }
@@ -827,6 +850,25 @@ function failureBlocks(s: string): string[]
827
850
 
828
851
  **Returns:** Each block, in order (empty when there are none).
829
852
 
853
+ #### `runningOutputSliceCommand(log, own)`
854
+
855
+ The shell command whose stdout is the interesting part of a log that is still being written:
856
+ the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
857
+
858
+ A Playwright `list` run prints one line per test as it finishes and its error blocks only at
859
+ the end, so the LAST bytes of a running log are its least informative part — in X1 flash-m61
860
+ the last 3 000 bytes of a 139-test walk were the skip list, and all 61 failures were above
861
+ them, which cost the run the grep it had to make anyway.
862
+
863
+ ```typescript
864
+ function runningOutputSliceCommand(log: string, own?: string): string
865
+ ```
866
+
867
+ - `log` — The task log path.
868
+ - `own` — A file the command redirects its own output to, if any.
869
+
870
+ **Returns:** The shell command, safe to hand to the execution backend as-is.
871
+
830
872
  #### `whitespaceTolerantReplace(content, oldString, newString)`
831
873
 
832
874
  Attempt a whitespace-tolerant replacement when an exact `old_string` match
@@ -1031,6 +1073,31 @@ const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
1031
1073
  ]
1032
1074
  ```
1033
1075
 
1076
+ #### `RUNNING_FAILURE_LINES`
1077
+
1078
+ Failure lines shown from a log that is still being written.
1079
+
1080
+ ```typescript
1081
+ const RUNNING_FAILURE_LINES: 40
1082
+ ```
1083
+
1084
+ #### `RUNNING_TAIL_BYTES`
1085
+
1086
+ Bytes of a running log's end appended after its failure lines.
1087
+
1088
+ ```typescript
1089
+ const RUNNING_TAIL_BYTES: 1000
1090
+ ```
1091
+
1092
+ #### `RUNNING_FAILURE_LINE_RE`
1093
+
1094
+ The lines worth showing from a log that is still being written: a failed test's mark or
1095
+ numbered block header, and a runner's own error lines. Matched with `grep -E`.
1096
+
1097
+ ```typescript
1098
+ const RUNNING_FAILURE_LINE_RE: '^[[:space:]]*(✘|✗|×|✖|✕|[0-9]+\\)|[0-9]+ (passed|failed|flaky|skipped|did not run)|FAIL|FAILED|ERROR|Error:|error:|Traceback|AssertionError)'
1099
+ ```
1100
+
1034
1101
  #### `PARSE_CHECKABLE`
1035
1102
 
1036
1103
  Files a parse check can vouch for cheaply right after a write.
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA0CnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAooDxF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA6CnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAupDxF"}
package/dist/tools.js CHANGED
@@ -25,7 +25,7 @@ const waitedByTask = new Map();
25
25
  const logBytesByTask = new Map();
26
26
  /** The file a background command writes its own output to, when it redirects it. */
27
27
  const outputFileByTask = new Map();
28
- import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, fusedLine, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
28
+ import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, fusedLine, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, redactSecrets, redactSecretsInCode, resolvePath, RUNNING_FAILURE_LINES, runningOutputSliceCommand, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
29
29
  /**
30
30
  * How long exec_command waits for a command it backgrounded only because the
31
31
  * command asked for more than the tool's ceiling, before handing back a task id.
@@ -921,7 +921,15 @@ export function buildTools(backend, config) {
921
921
  ...(fusedNote
922
922
  ? { syntaxError: fusedNote }
923
923
  : {
924
- ...(dropped.length > 0
924
+ // The evidence for this note is `located`, not `dropped`: a drop joins
925
+ // two lines only when something other than a newline (or EOF) follows
926
+ // the replaced text. A weaker model writes one line's change as two
927
+ // replacements — the line including its trailing newline, then the
928
+ // line's own text — and the newline it consumed ended that line with a
929
+ // BLANK line after it, so nothing joined while the model was told it
930
+ // had: it spent a new round trip putting the blank line back
931
+ // (flash-m62). Say nothing when nothing was joined.
932
+ ...(dropped.length > 0 && located.length > 0
925
933
  ? {
926
934
  newlineNote: `${dropped.length === 1 ? 'A replacement' : `${dropped.length} replacements`} removed the line break after the replaced text (old_string ended with a newline, new_string does not), so the next line now continues on the same line. Check the excerpt; add the newline back if that was not intended.`,
927
935
  }
@@ -966,7 +974,9 @@ export function buildTools(backend, config) {
966
974
  // search found nothing (X1 run sol-m07). A pattern that is not a valid
967
975
  // extended regex (`foo(` — an unbalanced paren) makes grep exit 2; it is
968
976
  // then searched as the plain text it was meant as.
969
- const grepArgs = `-rn ${globArg} ${contextArg} ${grepExcludeArgs(path)} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)}`;
977
+ // -H: a search scoped to one file printed `line:content` with no file, and every match
978
+ // came back `{file: "", line: 0}` (X1 flash-m62).
979
+ const grepArgs = `-rnH ${globArg} ${contextArg} ${grepExcludeArgs(path)} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)}`;
970
980
  const result = await backend.run(`{ grep -E ${grepArgs}; s=$?; if [ "$s" -eq 2 ]; then grep -F ${grepArgs}; fi; } 2>/dev/null || true`, { timeout: 10000 });
971
981
  // grep emits `<file>:<line>:<content>`, so redaction runs PER MATCH on the
972
982
  // content alone: the file prefix would otherwise hide an env assignment from
@@ -1268,7 +1278,12 @@ export function buildTools(backend, config) {
1268
1278
  // truncateMiddle (not truncate): a failing build/test/migration puts its
1269
1279
  // error at the TAIL, so keep the head AND the tail — head-only truncation
1270
1280
  // strands the executor with passing progress and no failure reason.
1271
- const stdout = sanitizeOutput(truncateMiddle(result.stdout, MAX_OUTPUT_SIZE));
1281
+ // A command that only prints source files returns file content: the code-safe grade,
1282
+ // as read_file uses (utilities.ts printsOnlySource).
1283
+ const grade = printsOnlySource(command)
1284
+ ? (s) => sanitizeFileContent(s, 'printed.ts')
1285
+ : sanitizeOutput;
1286
+ const stdout = grade(truncateMiddle(result.stdout, MAX_OUTPUT_SIZE));
1272
1287
  const stderr = sanitizeOutput(truncateMiddle(result.stderr, MAX_OUTPUT_SIZE));
1273
1288
  if (budgetSeconds && result.exitCode === 124) {
1274
1289
  return {
@@ -1456,15 +1471,17 @@ export function buildTools(backend, config) {
1456
1471
  };
1457
1472
  }
1458
1473
  // The output so far rides the result: a wait that shows nothing was waited blind
1459
- // (X1 flash-m50: 4m42s across two waits on an 11-minute run).
1474
+ // (X1 flash-m50: 4m42s across two waits on an 11-minute run). The slice leads with the
1475
+ // failure lines, not the last bytes — a suite's tail is its progress lines
1476
+ // (X1 flash-m61: the last 3 000 bytes of a 139-test walk were the skip list).
1460
1477
  const own = outputFileByTask.get(taskId);
1461
1478
  let soFar;
1462
1479
  try {
1463
- const r = await backend.run(`cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null | tail -c 3000`, { timeout: 5_000 });
1480
+ const r = await backend.run(runningOutputSliceCommand(log, own), { timeout: 5_000 });
1464
1481
  soFar = r.stdout ?? '';
1465
1482
  }
1466
1483
  catch (_error) {
1467
- // No tail: the note still says how to peek.
1484
+ // No slice: the note still says how to peek.
1468
1485
  soFar = '';
1469
1486
  }
1470
1487
  return {
@@ -1473,8 +1490,13 @@ export function buildTools(backend, config) {
1473
1490
  waitedMs,
1474
1491
  ...(soFar.trim() ? { outputSoFar: sanitizeOutput(soFar) } : {}),
1475
1492
  note: `Still running after ${Math.round(waitedMs / 1000)}s. Call wait_for_task again to ` +
1476
- `keep waiting — its result carries the log output${soFar.trim() ? '; outputSoFar is the end of the log as it stands, so there is no need to tail it' : ''}. ` +
1477
- `Do other work in the meantime (another suite, a fix you have already found) rather than waiting.`,
1493
+ `keep waiting — its result carries the log output. ` +
1494
+ (soFar.trim()
1495
+ ? `outputSoFar is the failure lines so far (up to ${RUNNING_FAILURE_LINES}) and then the end of ` +
1496
+ `the log, not the whole run — \`grep -n '✘' ${log} | head -40\` lists every failure, ` +
1497
+ `\`grep -n -A 20 '<test name>' ${log}\` reads one failure's error. `
1498
+ : `Nothing has been printed yet — \`grep -n '✘' ${log} | head -40\` lists failures as they appear. `) +
1499
+ `Meanwhile do work that leaves the code under test alone (read the failures so far, plan the fixes): editing app/src or api/src now changes the code the run is testing, and its result no longer counts.`,
1478
1500
  };
1479
1501
  }
1480
1502
  let output;
@@ -55,6 +55,17 @@ export declare function redactSecrets(s: string): string;
55
55
  * @returns A redacted copy that preserves ordinary code verbatim.
56
56
  */
57
57
  export declare function redactSecretsInCode(s: string): string;
58
+ /**
59
+ * Whether a shell command only PRINTS source files (`cat a.tsx`, `sed -n '1,40p' x.ts`,
60
+ * `head -50 b.hurl`): its output is file content and gets the code-safe redaction, like
61
+ * read_file. Through the env-dump grade a printed `type AuthState = 'authenticated' | …`
62
+ * came back as `'[REDACTED]'`, and the executor wrote the mask into a new file (X1
63
+ * flash-m58). Any segment that is not such a print, or any env file named, keeps the full grade.
64
+ *
65
+ * @param command - The shell command.
66
+ * @returns True when every segment prints source files only.
67
+ */
68
+ export declare function printsOnlySource(command: string): boolean;
58
69
  /**
59
70
  * Whether a path is an env file, for which {@link redactSecrets}' full env-dump
60
71
  * treatment is appropriate rather than {@link redactSecretsInCode}. Matches
@@ -108,9 +119,16 @@ export declare function outputRedirectTarget(command: string): string | null;
108
119
  * - a trailing `| tail -N`, which holds everything back until the command ends.
109
120
  * It is dropped: `wait_for_task` already returns the end of the log.
110
121
  * - `playwright test` with no `--reporter`, which runs the config's reporter —
111
- * often `html`, which prints nothing per test. `--reporter=line` is added
122
+ * often `html`, which prints nothing per test. `--reporter=list` is added
112
123
  * when the playwright run is the command's last step and nothing is piped or
113
- * redirected from it ( aside); the results are the same, printed as they happen.
124
+ * redirected from it: it writes each result as it finishes, and it is the
125
+ * shape the platform reads a background suite back in (`suite-fail-fast.ts`'s
126
+ * fail-fast note and the Tests bar both parse `list` lines). A run that names
127
+ * `--reporter=line` is switched to `list` for the same reason — `line` writes
128
+ * `[n/N] <title>` with no ✓/✘ mark and no duration, so on X1 flash-m62 an
129
+ * 8.3-minute walk of one suite could never trip the fail-fast note.
130
+ *
131
+ * Only the command's last step is touched, and only its reporter.
114
132
  *
115
133
  * @param command - The command as the executor sent it.
116
134
  * @returns The command to run and one sentence per change made (empty when none).
@@ -296,6 +314,29 @@ export declare function truncateMiddle(s: string, maxLength: number): string;
296
314
  * @returns Each block, in order (empty when there are none).
297
315
  */
298
316
  export declare function failureBlocks(s: string): string[];
317
+ /** Failure lines shown from a log that is still being written. */
318
+ export declare const RUNNING_FAILURE_LINES = 40;
319
+ /** Bytes of a running log's end appended after its failure lines. */
320
+ export declare const RUNNING_TAIL_BYTES = 1000;
321
+ /**
322
+ * The lines worth showing from a log that is still being written: a failed test's mark or
323
+ * numbered block header, and a runner's own error lines. Matched with `grep -E`.
324
+ */
325
+ export declare const RUNNING_FAILURE_LINE_RE = "^[[:space:]]*(\u2718|\u2717|\u00D7|\u2716|\u2715|[0-9]+\\)|[0-9]+ (passed|failed|flaky|skipped|did not run)|FAIL|FAILED|ERROR|Error:|error:|Traceback|AssertionError)";
326
+ /**
327
+ * The shell command whose stdout is the interesting part of a log that is still being written:
328
+ * the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
329
+ *
330
+ * A Playwright `list` run prints one line per test as it finishes and its error blocks only at
331
+ * the end, so the LAST bytes of a running log are its least informative part — in X1 flash-m61
332
+ * the last 3 000 bytes of a 139-test walk were the skip list, and all 61 failures were above
333
+ * them, which cost the run the grep it had to make anyway.
334
+ *
335
+ * @param log - The task log path.
336
+ * @param own - A file the command redirects its own output to, if any.
337
+ * @returns The shell command, safe to hand to the execution backend as-is.
338
+ */
339
+ export declare function runningOutputSliceCommand(log: string, own?: string): string;
299
340
  /**
300
341
  * Attempt a whitespace-tolerant replacement when an exact `old_string` match
301
342
  * failed. Finds a contiguous run of lines in `content` whose per-line
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
package/dist/utilities.js CHANGED
@@ -98,9 +98,15 @@ const NON_SECRET_VALUES = new Set([
98
98
  * @param quote - The quote character the value uses (`"` or `'`).
99
99
  * @returns A String.replace replacer preserving allowlisted values.
100
100
  */
101
- const jsonValueReplacer = (quote) => (match, prefix) => {
101
+ const jsonValueReplacer = (quote) => (match, prefix, offset, whole) => {
102
102
  const value = match.slice(prefix.length);
103
103
  const inner = value.slice(1, -1);
104
+ // A TypeScript type literal is never a credential: `type AuthState = 'authenticated' | …`
105
+ // (X1 flash-m58). A union member is followed by `|`; a declaration starts `type`.
106
+ if (/^\s*\|/.test(whole.slice(offset + match.length)))
107
+ return match;
108
+ if (/\btype\s+$/.test(whole.slice(Math.max(0, offset - 16), offset)))
109
+ return match;
104
110
  // An empty value hides nothing, and a suite fixture's `"password": ""` is the fact a
105
111
  // failing assertion turns on (X1 flash-m52).
106
112
  // A version (`"jsonwebtoken": "9.0.2"`, a package name that holds a keyword) is not a
@@ -153,6 +159,61 @@ export function redactSecrets(s) {
153
159
  export function redactSecretsInCode(s) {
154
160
  return s.replace(SECRET_ENV_ASSIGNMENT, '$1=[REDACTED]');
155
161
  }
162
+ /** Source-file extensions a printing command's output is read as code for. */
163
+ const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|css|scss|html|vue|svelte|md|sql|hurl)$/;
164
+ /**
165
+ * Whether a shell command only PRINTS source files (`cat a.tsx`, `sed -n '1,40p' x.ts`,
166
+ * `head -50 b.hurl`): its output is file content and gets the code-safe redaction, like
167
+ * read_file. Through the env-dump grade a printed `type AuthState = 'authenticated' | …`
168
+ * came back as `'[REDACTED]'`, and the executor wrote the mask into a new file (X1
169
+ * flash-m58). Any segment that is not such a print, or any env file named, keeps the full grade.
170
+ *
171
+ * @param command - The shell command.
172
+ * @returns True when every segment prints source files only.
173
+ */
174
+ export function printsOnlySource(command) {
175
+ if (/\.env\b/.test(command))
176
+ return false;
177
+ const segments = command
178
+ .split(/\s*(?:&&|;|\n)\s*/)
179
+ .map((s) => s.trim())
180
+ .filter((s) => s && !/^cd\s/.test(s));
181
+ if (segments.length === 0)
182
+ return false;
183
+ return segments.every((seg) => {
184
+ // Words, quotes kept whole: a grep pattern like "sign\\|verify" is one word, not a pipe.
185
+ const words = seg.match(/"(?:\\.|[^"\\])*"|'[^']*'|\S+/g) ?? [];
186
+ const stages = [[]];
187
+ for (const w of words) {
188
+ if (w === '|')
189
+ stages.push([]);
190
+ else if (!/^\d?>&\d$|^2>\/dev\/null$/.test(w))
191
+ stages[stages.length - 1].push(w);
192
+ }
193
+ const [first, ...rest] = stages;
194
+ const head = first[0];
195
+ // A print of named source files: cat/head/tail/nl/sed -n, or a non-recursive grep/rg
196
+ // (X1 flash-m61 read a fixture's password masked through `grep -n … auth.hurl`).
197
+ let args = first.slice(1);
198
+ if (head === 'sed' && args[0] !== '-n')
199
+ return false;
200
+ if (head === 'grep' || head === 'rg') {
201
+ if (args.some((a) => /^-[a-zA-Z]*[rR]/.test(a) || a === '--recursive'))
202
+ return false;
203
+ const flags = args.filter((a) => a.startsWith('-'));
204
+ const rest2 = args.filter((a) => !a.startsWith('-'));
205
+ args = flags.includes('-e') ? rest2 : rest2.slice(1);
206
+ }
207
+ else if (!/^(?:cat|head|tail|nl|sed)$/.test(head ?? ''))
208
+ return false;
209
+ const files = args.filter((t) => !t.startsWith('-') && !/^'[^']*'$|^"[^"]*"$|^\d+$|^\d+,\d+p$/.test(t));
210
+ if (files.length === 0 || !files.every((f) => SOURCE_FILE_RE.test(f)))
211
+ return false;
212
+ // After it, only text filters that read no file of their own.
213
+ return rest.every((stage) => /^(?:head|tail|sort|uniq|wc|cut|nl|grep)$/.test(stage[0] ?? '') &&
214
+ !stage.slice(1).some((a) => !a.startsWith('-') && /\.[a-z]+$/i.test(a) && !/^['"]/.test(a)));
215
+ });
216
+ }
156
217
  /**
157
218
  * Whether a path is an env file, for which {@link redactSecrets}' full env-dump
158
219
  * treatment is appropriate rather than {@link redactSecretsInCode}. Matches
@@ -226,6 +287,8 @@ export function outputRedirectTarget(command) {
226
287
  }
227
288
  /** A trailing `| tail -N` / `| tail -n N` (stderr redirects before it stay). */
228
289
  const TRAILING_TAIL = /\s*\|\s*tail(?:\s+-n)?\s+[-+]?\d+\s*$/;
290
+ /** `--reporter=line` / `--reporter line`, the one reporter the platform cannot read back. */
291
+ const LINE_REPORTER_FLAG = /--reporter([= ])line\b/;
229
292
  /**
230
293
  * Rewrite a command about to run in the background so its log grows while it
231
294
  * runs. Two shapes left the log empty until exit, and an empty log is what
@@ -235,9 +298,16 @@ const TRAILING_TAIL = /\s*\|\s*tail(?:\s+-n)?\s+[-+]?\d+\s*$/;
235
298
  * - a trailing `| tail -N`, which holds everything back until the command ends.
236
299
  * It is dropped: `wait_for_task` already returns the end of the log.
237
300
  * - `playwright test` with no `--reporter`, which runs the config's reporter —
238
- * often `html`, which prints nothing per test. `--reporter=line` is added
301
+ * often `html`, which prints nothing per test. `--reporter=list` is added
239
302
  * when the playwright run is the command's last step and nothing is piped or
240
- * redirected from it ( aside); the results are the same, printed as they happen.
303
+ * redirected from it: it writes each result as it finishes, and it is the
304
+ * shape the platform reads a background suite back in (`suite-fail-fast.ts`'s
305
+ * fail-fast note and the Tests bar both parse `list` lines). A run that names
306
+ * `--reporter=line` is switched to `list` for the same reason — `line` writes
307
+ * `[n/N] <title>` with no ✓/✘ mark and no duration, so on X1 flash-m62 an
308
+ * 8.3-minute walk of one suite could never trip the fail-fast note.
309
+ *
310
+ * Only the command's last step is touched, and only its reporter.
241
311
  *
242
312
  * @param command - The command as the executor sent it.
243
313
  * @returns The command to run and one sentence per change made (empty when none).
@@ -259,12 +329,20 @@ export function streamingBackgroundCommand(command) {
259
329
  changes.push(`Dropped the trailing \`${head[0].trim()}\`: Playwright prints its pass/fail count last, and it would be cut.`);
260
330
  }
261
331
  const lastStep = out.split(/&&|\|\||;/).at(-1) ?? '';
262
- if (/\bplaywright\s+test\b/.test(lastStep) &&
263
- !/--reporter\b/.test(lastStep) &&
332
+ const finalPlaywrightRun = /\bplaywright\s+test\b/.test(lastStep) &&
264
333
  // `2>&1` only joins stderr to the log; any other pipe or redirect sends the output elsewhere.
265
- !/[|>]/.test(lastStep.replace(/\d?>&\d/g, ''))) {
266
- out = `${out.trimEnd()} --reporter=line`;
267
- changes.push('Added `--reporter=line` so each test result is written to the log as it finishes ' +
334
+ !/[|>]/.test(lastStep.replace(/\d?>&\d/g, ''));
335
+ if (finalPlaywrightRun && LINE_REPORTER_FLAG.test(lastStep)) {
336
+ // Only the last step is rewritten, and `lastStep` is a suffix of `out`.
337
+ out =
338
+ out.slice(0, out.length - lastStep.length) +
339
+ lastStep.replace(LINE_REPORTER_FLAG, '--reporter$1list');
340
+ changes.push('Switched `--reporter=line` to `--reporter=list`: it also writes each test result as it ' +
341
+ 'finishes, and it is the shape the platform reads back from a background run.');
342
+ }
343
+ else if (finalPlaywrightRun && !/--reporter\b/.test(lastStep)) {
344
+ out = `${out.trimEnd()} --reporter=list`;
345
+ changes.push('Added `--reporter=list` so each test result is written to the log as it finishes ' +
268
346
  '(the config reporter may print nothing until the end).');
269
347
  }
270
348
  return { command: out, changes };
@@ -283,7 +361,8 @@ export function streamingBackgroundCommand(command) {
283
361
  * a keyless build still succeeds, and blocking those sent it looking for
284
362
  * workarounds (X0 rehearsal 14).
285
363
  */
286
- const BLOCKED_COMMANDS = /(?:^|[;&|`]\s*|(?:sh|bash|zsh|dash)\s+-c\s+['"]?\s*)(?:\/usr\/bin\/)?(?:\benv(?:\s+(?:-0|--null|--))?\s*(?:$|[;&|>)`'"])|\bprintenv\b|\bexport\s*$|\bset\s*$|\bdeclare\s+-x|cat\s+\/etc\/environment|cat\s+\/root\/\.bashrc|cat\s+\/proc\/\d+\/environ|cat\s+\/proc\/self\/environ|strings\s+\/proc|xargs[^;&|\n]*\/proc\/[^;&|\n]*environ|less\s+\/proc|head\s+\/proc|tail\s+\/proc|xxd\s+\/proc|od\s+\/proc|base64\s+\/proc|dd\s[^\n]*\/proc|sed\s[^\n]*\/proc\/[^\n]*environ|awk\s[^\n]*\/proc\/[^\n]*environ|cp\s[^\n]*\/proc\/[^\n]*environ|(?:cat|less|head|tail|xxd|od|base64|strings|dd|cp|sed|awk)\s[^\n;&|]*\/etc\/mol(?:\/|\b))/i;
364
+ // `(?<!\\)`: a backslash-escaped `\|` is a literal in the shell (grep "a\|env"), not a pipe (X1 flash-m58).
365
+ const BLOCKED_COMMANDS = /(?<!\\)(?:^|[;&|`]\s*|(?:sh|bash|zsh|dash)\s+-c\s+['"]?\s*)(?:\/usr\/bin\/)?(?:\benv(?:\s+(?:-0|--null|--))?\s*(?:$|[;&|>)`'"])|\bprintenv\b|\bexport\s*$|\bset\s*$|\bdeclare\s+-x|cat\s+\/etc\/environment|cat\s+\/root\/\.bashrc|cat\s+\/proc\/\d+\/environ|cat\s+\/proc\/self\/environ|strings\s+\/proc|xargs[^;&|\n]*\/proc\/[^;&|\n]*environ|less\s+\/proc|head\s+\/proc|tail\s+\/proc|xxd\s+\/proc|od\s+\/proc|base64\s+\/proc|dd\s[^\n]*\/proc|sed\s[^\n]*\/proc\/[^\n]*environ|awk\s[^\n]*\/proc\/[^\n]*environ|cp\s[^\n]*\/proc\/[^\n]*environ|(?:cat|less|head|tail|xxd|od|base64|strings|dd|cp|sed|awk)\s[^\n;&|]*\/etc\/mol(?:\/|\b))/i;
287
366
  /** Block shell redirects from /proc environ. */
288
367
  const BLOCKED_PROC_REDIRECT = /(?:<\s*\/proc\/(?:\d+|self)\/environ)/i;
289
368
  /** Block shell redirects from the molecule platform secrets dir (`/etc/mol`). */
@@ -554,6 +633,33 @@ export function truncate(s, maxLength) {
554
633
  export function truncateMiddle(s, maxLength) {
555
634
  if (s.length <= maxLength)
556
635
  return s;
636
+ // Room for the tally line it may append, so the result stays near the cap.
637
+ return withRunTally(s, truncateMiddleBody(s, Math.max(Math.floor(maxLength / 2), maxLength - 200)));
638
+ }
639
+ /**
640
+ * A test run's tally lines (` 3 failed`, ` 71 passed (4.6m)`) the cut text lost, put back at
641
+ * its end: Playwright lists every failed test after its "N failed" line, so a long list pushed
642
+ * the line itself into the cut middle and the run read as having no failures (X1 flash-m60).
643
+ *
644
+ * @param original - The whole output.
645
+ * @param cut - The truncated output.
646
+ * @returns The cut output, with any lost tally lines appended.
647
+ */
648
+ function withRunTally(original, cut) {
649
+ const tally = [
650
+ ...original.matchAll(/^\s*\d+ (?:failed|passed|flaky|skipped|did not run|interrupted)\b[^\n]{0,40}$/gm),
651
+ ].map((m) => m[0].trim());
652
+ const lost = tally.filter((line) => !cut.includes(line));
653
+ return lost.length ? `${cut}\n[run tally] ${lost.join(' · ')}` : cut;
654
+ }
655
+ /**
656
+ * {@link truncateMiddle} without the tally guard.
657
+ *
658
+ * @param s - The text.
659
+ * @param maxLength - The budget.
660
+ * @returns The cut text.
661
+ */
662
+ function truncateMiddleBody(s, maxLength) {
557
663
  // Reserve room for the elision notice so head + notice + tail stays WITHIN
558
664
  // maxLength — the cap is a real token budget the caller relies on (a head-only
559
665
  // truncate that overshoots by its suffix length breaks that contract). 140 =
@@ -624,6 +730,33 @@ export function failureBlocks(s) {
624
730
  blocks.push(current.join('\n').trimEnd());
625
731
  return blocks;
626
732
  }
733
+ /** Failure lines shown from a log that is still being written. */
734
+ export const RUNNING_FAILURE_LINES = 40;
735
+ /** Bytes of a running log's end appended after its failure lines. */
736
+ export const RUNNING_TAIL_BYTES = 1000;
737
+ /**
738
+ * The lines worth showing from a log that is still being written: a failed test's mark or
739
+ * numbered block header, and a runner's own error lines. Matched with `grep -E`.
740
+ */
741
+ export const RUNNING_FAILURE_LINE_RE = '^[[:space:]]*(✘|✗|×|✖|✕|[0-9]+\\)|[0-9]+ (passed|failed|flaky|skipped|did not run)|FAIL|FAILED|ERROR|Error:|error:|Traceback|AssertionError)';
742
+ /**
743
+ * The shell command whose stdout is the interesting part of a log that is still being written:
744
+ * the failure lines so far (capped at {@link RUNNING_FAILURE_LINES}), then the end of the log.
745
+ *
746
+ * A Playwright `list` run prints one line per test as it finishes and its error blocks only at
747
+ * the end, so the LAST bytes of a running log are its least informative part — in X1 flash-m61
748
+ * the last 3 000 bytes of a 139-test walk were the skip list, and all 61 failures were above
749
+ * them, which cost the run the grep it had to make anyway.
750
+ *
751
+ * @param log - The task log path.
752
+ * @param own - A file the command redirects its own output to, if any.
753
+ * @returns The shell command, safe to hand to the execution backend as-is.
754
+ */
755
+ export function runningOutputSliceCommand(log, own) {
756
+ const read = `cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null`;
757
+ return (`{ ${read} | grep -aE ${shellQuote(RUNNING_FAILURE_LINE_RE)} | head -${RUNNING_FAILURE_LINES} | cut -c1-300; ` +
758
+ `echo; ${read} | tail -c ${RUNNING_TAIL_BYTES}; }`);
759
+ }
627
760
  /**
628
761
  * Attempt a whitespace-tolerant replacement when an exact `old_string` match
629
762
  * failed. Finds a contiguous run of lines in `content` whose per-line
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.5.15",
3
+ "version": "1.5.16",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",