@molecule/api-ai-tools 1.5.19 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Command position: which program a shell segment actually RUNS, and which words are only
3
+ * arguments to something else.
4
+ *
5
+ * Every classifier that read a command as "this runs the suite" matched the runner's NAME
6
+ * anywhere in the text, so a runner named in an argument was read as an invocation: X1
7
+ * xr-molecule-flash-m81 (+20:03) sent
8
+ * `cd /workspace/fixtures/realworld/specs/api && ls && head -30 run-api-tests-hurl.sh` — two
9
+ * reads of a file that was already there — and the suite-overlap hold answered "a Playwright
10
+ * suite is already running against this app and database … wait for it", costing a minute of
11
+ * forced idling on a command that writes nothing and runs nothing. `cat`, `grep`, `sed -n`,
12
+ * `wc`, `ls` and `git show` of a runner script are the same shape.
13
+ *
14
+ * So this parser answers it in one place, for every caller: split the command at its pipeline
15
+ * and statement separators, take each segment's word in command position — after the wrapper
16
+ * prefixes a shell puts in front of it (`HOST=x sudo nice -n 10 npx`, `bash`, `timeout 280`) —
17
+ * and let the caller match its runner against THAT segment's text, never against the raw
18
+ * command. `{@link commandSegments}` returns the segments, and
19
+ * {@link runsInCommandPosition} asks one anchored pattern of them all.
20
+ *
21
+ * Not command position, and therefore not an invocation: a runner named as an argument
22
+ * (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
23
+ * `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
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).
26
+ *
27
+ * @module
28
+ */
29
+ /** A command's words, quotes kept whole — the same reading a printed-source check uses. */
30
+ const WORD = /"(?:\\.|[^"\\])*"|'[^']*'|\S+/g;
31
+ /** Shell words that stand alone: grouping, never an argument. */
32
+ const GROUPING = new Set(['(', ')', '{', '}']);
33
+ /** How deep a `sh -c '<command>'` is opened before the parser leaves it alone. */
34
+ const MAX_NESTING = 3;
35
+ /** How many wrapper words one segment may carry before the parser gives up on it. */
36
+ const MAX_WRAPPERS = 32;
37
+ /**
38
+ * Words that run in front of the real program: a scheduler, a privilege wrapper, a process
39
+ * launcher, or a name that simply runs the next word (`command ls`, `exec ls`). All of them
40
+ * leave what runs unchanged, so all of them are stripped.
41
+ */
42
+ const LAUNCHERS = new Set([
43
+ 'command',
44
+ 'doas',
45
+ 'env',
46
+ 'exec',
47
+ 'ionice',
48
+ 'nice',
49
+ 'nohup',
50
+ 'setsid',
51
+ 'sudo',
52
+ 'time',
53
+ 'timeout',
54
+ ]);
55
+ /** The shells whose `-c '<command>'` argument is a command of its own. */
56
+ const SHELLS = new Set(['ash', 'bash', 'dash', 'ksh', 'sh', 'zsh']);
57
+ /** Package runners that run the program named after them (`npx playwright test`). */
58
+ const PACKAGE_RUNNERS = new Set(['bunx', 'npx', 'pnpx']);
59
+ /** Package managers whose sub-command runs a package's binary (`bun x`, `pnpm dlx`, `npm exec`). */
60
+ const PACKAGE_RUNNER_HEADS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
61
+ /**
62
+ * The sub-commands that run a package's binary, after a {@link PACKAGE_RUNNER_HEADS} word. Its
63
+ * other sub-commands are the program itself, not a wrapper: `yarn test` and `bun run build` run
64
+ * a script of the project's, and nothing is stripped from them.
65
+ */
66
+ const PACKAGE_RUNNER_SUBCOMMANDS = new Set(['dlx', 'exec', 'x']);
67
+ /** Wrapper flags that are followed by their own value (`sudo -u root`, `timeout -k 5`). */
68
+ const WRAPPER_VALUE_FLAGS = {
69
+ env: ['-S', '--split-string', '-u', '--unset', '-C', '--chdir'],
70
+ ionice: ['-c', '--class', '-n', '--classdata', '-p', '--pid', '-P', '--pgid', '-u', '--uid'],
71
+ nice: ['-n', '--adjustment'],
72
+ sudo: [
73
+ '-C',
74
+ '--chdir',
75
+ '-g',
76
+ '--group',
77
+ '-p',
78
+ '--prompt',
79
+ '-r',
80
+ '--role',
81
+ '-t',
82
+ '--type',
83
+ '-u',
84
+ '--user',
85
+ '-U',
86
+ '--other-user',
87
+ ],
88
+ time: ['-f', '--format', '-o', '--output'],
89
+ timeout: ['-k', '--kill-after', '-s', '--signal'],
90
+ };
91
+ /** An environment assignment (`HOST=…`, `BASE_URL="…"`): it runs nothing of its own. */
92
+ const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
93
+ /** A wrapper's own duration — `timeout 280 …`, `timeout 1m …` — which is not a flag. */
94
+ const DURATION = /^\d+(?:\.\d+)?[smhd]?$/;
95
+ /**
96
+ * A word without its shell quotes.
97
+ *
98
+ * @param word - One word of a segment.
99
+ * @returns The word with a leading and a trailing quote character removed.
100
+ */
101
+ function unquoted(word) {
102
+ return word.replace(/^['"]|['"]$/g, '');
103
+ }
104
+ /**
105
+ * A word's program name: quotes removed, directory removed (`/usr/bin/hurl` → `hurl`). A scoped
106
+ * package keeps its scope — the `/` in `@usebruno/cli` is part of the name, not a directory.
107
+ *
108
+ * @param word - One word of a segment.
109
+ * @returns The bare name.
110
+ */
111
+ function programName(word) {
112
+ const bare = unquoted(word);
113
+ if (bare.startsWith('@'))
114
+ return bare;
115
+ const parts = bare.split('/');
116
+ return parts[parts.length - 1] ?? '';
117
+ }
118
+ /**
119
+ * How many of a wrapper's own words follow it — its flags, and each value flag's value.
120
+ *
121
+ * @param words - The segment's words.
122
+ * @param from - The index just after the wrapper.
123
+ * @param wrapper - The wrapper's program name.
124
+ * @returns The count of words the wrapper owns.
125
+ */
126
+ function flagWords(words, from, wrapper) {
127
+ const withValue = WRAPPER_VALUE_FLAGS[wrapper] ?? [];
128
+ let count = 0;
129
+ while (from + count < words.length && (words[from + count] ?? '').startsWith('-')) {
130
+ const flag = words[from + count] ?? '';
131
+ // `--flag=value` carries its value inside the same word.
132
+ count += !flag.includes('=') && withValue.includes(flag) ? 2 : 1;
133
+ }
134
+ return count;
135
+ }
136
+ /**
137
+ * The `-c '<command>'` string a shell was handed, or null when the shell was given a FILE
138
+ * (`bash run.sh`) — the wrapper walk then steps past its flags instead.
139
+ *
140
+ * @param words - The segment's words.
141
+ * @param from - The index just after the shell's name.
142
+ * @returns The command string unquoted, or null.
143
+ */
144
+ function nestedCommand(words, from) {
145
+ for (let i = from; i < words.length; i++) {
146
+ const word = words[i] ?? '';
147
+ if (!word.startsWith('-'))
148
+ return null;
149
+ // A flag cluster holding `c` is `-c` (`bash -lc '…'`, `sh -ec '…'`).
150
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(word) || word === '--command') {
151
+ const next = words[i + 1];
152
+ return next === undefined ? null : unquoted(next);
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+ /**
158
+ * Where the program starts in one segment's words.
159
+ *
160
+ * @param words - The segment's words, quotes kept.
161
+ * @returns The index of the word in command position (one past the end when the segment runs
162
+ * nothing), and the command string of a `sh -c '<command>'` this segment turns out to be.
163
+ */
164
+ function commandStart(words) {
165
+ let at = 0;
166
+ for (let guard = 0; guard < MAX_WRAPPERS && at < words.length; guard++) {
167
+ const word = words[at] ?? '';
168
+ if (ENV_ASSIGNMENT.test(word)) {
169
+ at += 1;
170
+ continue;
171
+ }
172
+ const name = programName(word);
173
+ // `! cmd` inverts an exit status: what runs is still the word after it.
174
+ if (name === '!') {
175
+ at += 1;
176
+ continue;
177
+ }
178
+ if (SHELLS.has(name)) {
179
+ const nested = nestedCommand(words, at + 1);
180
+ if (nested !== null)
181
+ return { at, nested };
182
+ at += 1 + flagWords(words, at + 1, name);
183
+ continue;
184
+ }
185
+ if (PACKAGE_RUNNERS.has(name)) {
186
+ at += 1 + flagWords(words, at + 1, name);
187
+ continue;
188
+ }
189
+ if (PACKAGE_RUNNER_HEADS.has(name) &&
190
+ PACKAGE_RUNNER_SUBCOMMANDS.has(unquoted(words[at + 1] ?? ''))) {
191
+ at += 2 + flagWords(words, at + 2, name);
192
+ continue;
193
+ }
194
+ if (!LAUNCHERS.has(name))
195
+ return { at, nested: null };
196
+ at += 1 + flagWords(words, at + 1, name);
197
+ if (name === 'timeout' && DURATION.test(words[at] ?? ''))
198
+ at += 1;
199
+ }
200
+ return { at, nested: null };
201
+ }
202
+ /**
203
+ * A command's statements and pipeline stages, split where the shell splits them — inside quotes
204
+ * and inside a command substitution, a `|`, `;` or `&&` belongs to that command, not to this one.
205
+ *
206
+ * A single `&` separates only when it backgrounds a command: the one in a redirect (`2>&1`,
207
+ * `&>log`) is part of the word it sits in.
208
+ *
209
+ * @param command - The shell command.
210
+ * @returns Each segment's text, in order, with empty ones dropped.
211
+ */
212
+ function splitSegments(command) {
213
+ const parts = [];
214
+ let buffer = '';
215
+ let quote = null;
216
+ let substitutions = 0;
217
+ for (let i = 0; i < command.length; i++) {
218
+ const char = command[i] ?? '';
219
+ if (quote !== null) {
220
+ buffer += char;
221
+ if (char === '\\' && quote === '"' && i + 1 < command.length) {
222
+ buffer += command[i + 1] ?? '';
223
+ i += 1;
224
+ }
225
+ else if (char === quote)
226
+ quote = null;
227
+ continue;
228
+ }
229
+ if (char === '"' || char === "'") {
230
+ quote = char;
231
+ buffer += char;
232
+ continue;
233
+ }
234
+ if (char === '\\' && i + 1 < command.length) {
235
+ buffer += char + (command[i + 1] ?? '');
236
+ i += 1;
237
+ continue;
238
+ }
239
+ if (char === '$' && command[i + 1] === '(') {
240
+ substitutions += 1;
241
+ buffer += char;
242
+ continue;
243
+ }
244
+ if (substitutions > 0) {
245
+ if (char === '(')
246
+ substitutions += 1;
247
+ else if (char === ')')
248
+ substitutions -= 1;
249
+ buffer += char;
250
+ continue;
251
+ }
252
+ const next = command[i + 1];
253
+ if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
254
+ parts.push(buffer);
255
+ buffer = '';
256
+ i += 1;
257
+ continue;
258
+ }
259
+ if (char === ';' || char === '|' || char === '\n') {
260
+ parts.push(buffer);
261
+ buffer = '';
262
+ continue;
263
+ }
264
+ if (char === '&' && command[i - 1] !== '>' && next !== '>') {
265
+ parts.push(buffer);
266
+ buffer = '';
267
+ continue;
268
+ }
269
+ buffer += char;
270
+ }
271
+ parts.push(buffer);
272
+ return parts.map((part) => part.trim()).filter(Boolean);
273
+ }
274
+ /**
275
+ * One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
276
+ * against the first word taken off it (`(npx playwright test)`).
277
+ *
278
+ * @param text - One segment's text.
279
+ * @returns The words.
280
+ */
281
+ function segmentWords(text) {
282
+ const words = text.match(WORD) ?? [];
283
+ const cleaned = words.filter((word) => !GROUPING.has(word));
284
+ const first = cleaned[0];
285
+ if (first !== undefined) {
286
+ const bare = first.replace(/^[({]+/, '');
287
+ if (bare === '')
288
+ cleaned.shift();
289
+ else
290
+ cleaned[0] = bare;
291
+ }
292
+ return cleaned;
293
+ }
294
+ /**
295
+ * The segments of a command, each with its program and arguments — the answer every classifier
296
+ * that reads a command as "this runs the suite" is asking for.
297
+ *
298
+ * @param command - The shell command.
299
+ * @returns One entry per statement or pipeline stage that runs something, in order.
300
+ */
301
+ export function commandSegments(command) {
302
+ return segmentsOf(command, 0);
303
+ }
304
+ /**
305
+ * {@link commandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
306
+ * {@link MAX_NESTING} allows.
307
+ *
308
+ * @param command - The shell command.
309
+ * @param depth - How many `sh -c` strings this command already sits inside.
310
+ * @returns The segments.
311
+ */
312
+ function segmentsOf(command, depth) {
313
+ if (typeof command !== 'string' || depth > MAX_NESTING)
314
+ return [];
315
+ const out = [];
316
+ for (const text of splitSegments(command)) {
317
+ const words = segmentWords(text);
318
+ if (words.length === 0)
319
+ continue;
320
+ const start = commandStart(words);
321
+ if (start.nested !== null) {
322
+ out.push(...segmentsOf(start.nested, depth + 1));
323
+ continue;
324
+ }
325
+ const word = words[start.at];
326
+ // Only wrappers and assignments: nothing this segment runs.
327
+ if (word === undefined)
328
+ continue;
329
+ const program = programName(word);
330
+ const args = words.slice(start.at + 1);
331
+ out.push({ program, args, text: [program, ...args].join(' ') });
332
+ }
333
+ return out;
334
+ }
335
+ /**
336
+ * Whether any segment of the command runs a program whose command-position text matches
337
+ * `pattern` — the one question a runner-kind table asks.
338
+ *
339
+ * The pattern is matched against `program` and its arguments as one line, so it must be
340
+ * anchored at `^`; it must not carry the `g` flag (a pattern shared between calls would then
341
+ * carry its own match position from one segment to the next).
342
+ *
343
+ * @param command - The shell command.
344
+ * @param pattern - An anchored pattern for the runner's invocation.
345
+ * @returns True when a segment in command position matches.
346
+ */
347
+ export function runsInCommandPosition(command, pattern) {
348
+ return commandSegments(command).some((segment) => {
349
+ pattern.lastIndex = 0;
350
+ return pattern.test(segment.text);
351
+ });
352
+ }
package/dist/index.d.ts CHANGED
@@ -26,9 +26,13 @@
26
26
  * - **`read_file` takes `paths` as well as `path`.** An agent that issues one
27
27
  * tool call per model round-trip pays a full round-trip per file, so a project
28
28
  * survey costs minutes; batched, the same reads cost one call each 25 files
29
- * (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
30
- * stopped). Each entry carries its own content or its own error, so one bad
31
- * path never fails the batch.
29
+ * (`MAX_BATCH_READ_FILES`). One result is capped at `MAX_READ_CALL_CHARS`, the
30
+ * ceiling the request side puts on a single tool result: whole files are
31
+ * returned up to it and every path past that is named in `note` and listed as
32
+ * `not returned`, so a file is never handed back in part — the shape that lost
33
+ * the middle of a ten-file batch to the request-side cut (X0 flash-m81). Each
34
+ * entry carries its own content or its own error, so one bad path never fails
35
+ * the batch.
32
36
  * - **`edit_file` shows the result, so the model need not re-read.** A
33
37
  * successful edit returns `excerpt`: each edited region as it now reads, with
34
38
  * 1-based line numbers (`N: text`) and a few lines of context, capped at
@@ -116,7 +120,9 @@
116
120
  export * from './backends/local.js';
117
121
  export * from './backends/sandbox.js';
118
122
  export * from './browser-guard.js';
123
+ export * from './command-position.js';
119
124
  export * from './input-normalizer.js';
125
+ export * from './no-test-run.js';
120
126
  export * from './schemas.js';
121
127
  export * from './system-prompt.js';
122
128
  export * from './tools.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkHG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -26,9 +26,13 @@
26
26
  * - **`read_file` takes `paths` as well as `path`.** An agent that issues one
27
27
  * tool call per model round-trip pays a full round-trip per file, so a project
28
28
  * survey costs minutes; batched, the same reads cost one call each 25 files
29
- * (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
30
- * stopped). Each entry carries its own content or its own error, so one bad
31
- * path never fails the batch.
29
+ * (`MAX_BATCH_READ_FILES`). One result is capped at `MAX_READ_CALL_CHARS`, the
30
+ * ceiling the request side puts on a single tool result: whole files are
31
+ * returned up to it and every path past that is named in `note` and listed as
32
+ * `not returned`, so a file is never handed back in part — the shape that lost
33
+ * the middle of a ten-file batch to the request-side cut (X0 flash-m81). Each
34
+ * entry carries its own content or its own error, so one bad path never fails
35
+ * the batch.
32
36
  * - **`edit_file` shows the result, so the model need not re-read.** A
33
37
  * successful edit returns `excerpt`: each edited region as it now reads, with
34
38
  * 1-based line numbers (`N: text`) and a few lines of context, capped at
@@ -116,7 +120,9 @@
116
120
  export * from './backends/local.js';
117
121
  export * from './backends/sandbox.js';
118
122
  export * from './browser-guard.js';
123
+ export * from './command-position.js';
119
124
  export * from './input-normalizer.js';
125
+ export * from './no-test-run.js';
120
126
  export * from './schemas.js';
121
127
  export * from './system-prompt.js';
122
128
  export * from './tools.js';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The runner invocations that print information and run no test — the one classifier every
3
+ * site asks before it treats a command as a test run.
4
+ *
5
+ * A Playwright CLI has subcommands and flags that never start a test: `playwright test --list`
6
+ * prints the tests it would run and exits, `show-report`/`show-trace` open a saved report,
7
+ * `codegen` records one, `install` fetches browsers, `merge-reports` joins blobs, and
8
+ * `--help`/`--version` print and exit. Vitest lists with `vitest list` / `--list` and hurl
9
+ * prints with `--help`/`--version`, the same way.
10
+ *
11
+ * Every check in the platform read such a command as a whole-suite run, because each one
12
+ * matched the runner's NAME. X1 xr-molecule-flash-m79's first Playwright command was a config
13
+ * check — a config written to `/tmp`, `cp`'d beside the suite and used with `--list` — and it
14
+ * came back HELD, `status: not_run`, with a ~4 KB `suiteNote` telling it to send the same
15
+ * command with `--max-failures=15` ("uncapped it walks every test … 8–13 minutes"), beside an
16
+ * environment block saying the run "ran on their defaults" and an import note about a path the
17
+ * `cp` had already fixed. Nothing ran. The model gave up on the approach, wrote a second
18
+ * config, and its next command was held again (flash-m74's "same command three times" shape):
19
+ * two holds, zero tests, and the config check it asked for never answered.
20
+ *
21
+ * So the classifiers live here, in one place, and callers ask them instead of their own
22
+ * regexes: {@link commandRunsNoTests} answers for a whole command, and the runner-specific
23
+ * sites (molecule-dev's suite checks, `streamingBackgroundCommand`, `isSuiteRunCommand`) route
24
+ * through it. A command that runs no test is never held, never noted, and never rewritten
25
+ * (`--retries`, `--reporter`) as if it were a run.
26
+ *
27
+ * What counts as an invocation is COMMAND POSITION, read by `@molecule/api-ai-tools`'
28
+ * `commandSegments`: `npx playwright test --list`, `hurl --help` and `bash run.sh` invoke the
29
+ * runner, while a runner named as an argument (`grep -n playwright e2e/README.md`,
30
+ * `head -30 run-api-tests-hurl.sh`) invokes nothing at all. X1 xr-molecule-flash-m81 (+20:03)
31
+ * read a runner script with `head -30` and the suite-overlap hold answered "a Playwright suite is
32
+ * already running … wait for it", for a command that runs nothing.
33
+ *
34
+ * @module
35
+ */
36
+ /**
37
+ * Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
38
+ * command runs no test at all.
39
+ *
40
+ * A runner counts only where a shell would RUN it ({@link commandSegments}): the word in command
41
+ * position of a segment, after its wrapper prefixes. A runner named in an argument is not an
42
+ * invocation — `head -30 run-api-tests-hurl.sh` and `grep -n playwright e2e/README.md` name a
43
+ * file to read, and a `--list` written inside a file the command writes is not a flag anyone runs
44
+ * with (flash-m81).
45
+ *
46
+ * A command that invokes no runner is NOT one of these (false): "the command runs no test" is
47
+ * then true for `ls`, which is not the question any caller is asking. The question is whether
48
+ * the runner invocation each caller read as a run is one.
49
+ *
50
+ * A command that also runs tests is false however it is spelled: `npx playwright install &&
51
+ * npx playwright test` installs and runs, so a caller that sees `playwright test` in it is
52
+ * reading a real run.
53
+ *
54
+ * @param command - The shell command as the executor sent it.
55
+ * @returns True when the command invokes a runner only to print.
56
+ */
57
+ export declare function commandRunsNoTests(command: string): boolean;
58
+ //# sourceMappingURL=no-test-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-test-run.d.ts","sourceRoot":"","sources":["../src/no-test-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AA0EH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAU3D"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The runner invocations that print information and run no test — the one classifier every
3
+ * site asks before it treats a command as a test run.
4
+ *
5
+ * A Playwright CLI has subcommands and flags that never start a test: `playwright test --list`
6
+ * prints the tests it would run and exits, `show-report`/`show-trace` open a saved report,
7
+ * `codegen` records one, `install` fetches browsers, `merge-reports` joins blobs, and
8
+ * `--help`/`--version` print and exit. Vitest lists with `vitest list` / `--list` and hurl
9
+ * prints with `--help`/`--version`, the same way.
10
+ *
11
+ * Every check in the platform read such a command as a whole-suite run, because each one
12
+ * matched the runner's NAME. X1 xr-molecule-flash-m79's first Playwright command was a config
13
+ * check — a config written to `/tmp`, `cp`'d beside the suite and used with `--list` — and it
14
+ * came back HELD, `status: not_run`, with a ~4 KB `suiteNote` telling it to send the same
15
+ * command with `--max-failures=15` ("uncapped it walks every test … 8–13 minutes"), beside an
16
+ * environment block saying the run "ran on their defaults" and an import note about a path the
17
+ * `cp` had already fixed. Nothing ran. The model gave up on the approach, wrote a second
18
+ * config, and its next command was held again (flash-m74's "same command three times" shape):
19
+ * two holds, zero tests, and the config check it asked for never answered.
20
+ *
21
+ * So the classifiers live here, in one place, and callers ask them instead of their own
22
+ * regexes: {@link commandRunsNoTests} answers for a whole command, and the runner-specific
23
+ * sites (molecule-dev's suite checks, `streamingBackgroundCommand`, `isSuiteRunCommand`) route
24
+ * through it. A command that runs no test is never held, never noted, and never rewritten
25
+ * (`--retries`, `--reporter`) as if it were a run.
26
+ *
27
+ * What counts as an invocation is COMMAND POSITION, read by `@molecule/api-ai-tools`'
28
+ * `commandSegments`: `npx playwright test --list`, `hurl --help` and `bash run.sh` invoke the
29
+ * runner, while a runner named as an argument (`grep -n playwright e2e/README.md`,
30
+ * `head -30 run-api-tests-hurl.sh`) invokes nothing at all. X1 xr-molecule-flash-m81 (+20:03)
31
+ * read a runner script with `head -30` and the suite-overlap hold answered "a Playwright suite is
32
+ * already running … wait for it", for a command that runs nothing.
33
+ *
34
+ * @module
35
+ */
36
+ import { commandSegments } from './command-position.js';
37
+ /**
38
+ * The runners by the word in command position. A runner spelled as a FILE's name is not one:
39
+ * `-c playwright.local.config.ts` names a config, and `node playwright.js` runs node.
40
+ */
41
+ const RUNNERS = {
42
+ playwright: 'playwright',
43
+ vitest: 'vitest',
44
+ hurl: 'hurl',
45
+ };
46
+ /** Playwright's subcommands that start no test. */
47
+ const PLAYWRIGHT_INFO_SUBCOMMANDS = [
48
+ 'show-report',
49
+ 'show-trace',
50
+ 'codegen',
51
+ 'install',
52
+ 'merge-reports',
53
+ ];
54
+ /** Vitest's subcommand that prints what a run would collect. */
55
+ const VITEST_LIST_SUBCOMMAND = 'list';
56
+ /**
57
+ * The flags that put a runner in an information mode: it prints and exits. `-h` is `--help`
58
+ * for all three (hurl's manual documents `-h, --help`; Playwright's and Vitest's CLIs follow
59
+ * the same convention), and the value-taking flags beside them are why each is matched as a
60
+ * whole token — hurl's `-H` is a header, not help.
61
+ */
62
+ const NO_RUN_FLAGS = {
63
+ playwright: [
64
+ /(?:^|\s)--list(?=\s|$)/,
65
+ /(?:^|\s)--dry-run(?=\s|$)/,
66
+ /(?:^|\s)--help(?=\s|$)/,
67
+ /(?:^|\s)-h(?=\s|$)/,
68
+ /(?:^|\s)--version(?=\s|$)/,
69
+ ],
70
+ vitest: [
71
+ /(?:^|\s)--list(?=\s|$)/,
72
+ /(?:^|\s)--help(?=\s|$)/,
73
+ /(?:^|\s)-h(?=\s|$)/,
74
+ /(?:^|\s)--version(?=\s|$)/,
75
+ ],
76
+ hurl: [/(?:^|\s)--help(?=\s|$)/, /(?:^|\s)-h(?=\s|$)/, /(?:^|\s)--version(?=\s|$)/],
77
+ };
78
+ /**
79
+ * A heredoc body is data a command writes, not what it runs: a `playwright test --list` inside
80
+ * the config a command writes is not a run of this command either way. The pattern consumes the
81
+ * writer's own line prefix, the same shape `outputRedirectTarget` reads.
82
+ */
83
+ const HEREDOC = /[^\n;&|]*<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g;
84
+ /**
85
+ * Whether this runner's invocation — the text from just after its name to the end of its
86
+ * segment ({@link commandSegments}) — only prints.
87
+ *
88
+ * @param runner - The runner the segment invokes.
89
+ * @param after - Everything after the runner's name in that segment.
90
+ * @returns True when the invocation starts no test.
91
+ */
92
+ function informational(runner, after) {
93
+ const first = after.trimStart().split(/\s+/)[0] ?? '';
94
+ if (runner === 'playwright' && PLAYWRIGHT_INFO_SUBCOMMANDS.includes(first))
95
+ return true;
96
+ if (runner === 'vitest' && first === VITEST_LIST_SUBCOMMAND)
97
+ return true;
98
+ return NO_RUN_FLAGS[runner].some((re) => re.test(after));
99
+ }
100
+ /**
101
+ * Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
102
+ * command runs no test at all.
103
+ *
104
+ * A runner counts only where a shell would RUN it ({@link commandSegments}): the word in command
105
+ * position of a segment, after its wrapper prefixes. A runner named in an argument is not an
106
+ * invocation — `head -30 run-api-tests-hurl.sh` and `grep -n playwright e2e/README.md` name a
107
+ * file to read, and a `--list` written inside a file the command writes is not a flag anyone runs
108
+ * with (flash-m81).
109
+ *
110
+ * A command that invokes no runner is NOT one of these (false): "the command runs no test" is
111
+ * then true for `ls`, which is not the question any caller is asking. The question is whether
112
+ * the runner invocation each caller read as a run is one.
113
+ *
114
+ * A command that also runs tests is false however it is spelled: `npx playwright install &&
115
+ * npx playwright test` installs and runs, so a caller that sees `playwright test` in it is
116
+ * reading a real run.
117
+ *
118
+ * @param command - The shell command as the executor sent it.
119
+ * @returns True when the command invokes a runner only to print.
120
+ */
121
+ export function commandRunsNoTests(command) {
122
+ if (typeof command !== 'string' || !command.trim())
123
+ return false;
124
+ let invocations = 0;
125
+ for (const segment of commandSegments(command.replace(HEREDOC, ' '))) {
126
+ const runner = RUNNERS[segment.program];
127
+ if (!runner)
128
+ continue;
129
+ invocations += 1;
130
+ if (!informational(runner, segment.args.join(' ')))
131
+ return false;
132
+ }
133
+ return invocations > 0;
134
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAmU1C,CAAA"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAyU1C,CAAA"}
package/dist/schemas.js CHANGED
@@ -29,7 +29,8 @@ export const TOOL_SCHEMAS = {
29
29
  'gathering the files behind one feature — pass them all as `paths` in ONE call rather ' +
30
30
  'than calling this tool once per file: same content, a fraction of the time. ' +
31
31
  'For a @molecule package\u2019s docs call read_molecule_doc instead — one call, the whole ' +
32
- 'document. A file under 80 KB comes back whole; do not page through it with offset/limit.',
32
+ 'document. A file that fits one read result comes back whole; do not page through it with ' +
33
+ 'offset/limit.',
33
34
  parameters: {
34
35
  type: 'object',
35
36
  properties: {
@@ -42,7 +43,9 @@ export const TOOL_SCHEMAS = {
42
43
  items: { type: 'string' },
43
44
  description: 'Paths to SEVERAL files to read in one call — preferred whenever you want more ' +
44
45
  'than one file. Returns `files`, one entry per path in the order given, each ' +
45
- "with that file's content or its own error.",
46
+ "with that file's content or its own error. Whole files are returned up to what one " +
47
+ 'read result carries; every path past that comes back in the same list marked ' +
48
+ '`not returned` — read those next, in their own calls. No file is ever returned in part.',
46
49
  },
47
50
  offset: {
48
51
  type: 'number',
@@ -248,8 +251,11 @@ export const TOOL_SCHEMAS = {
248
251
  'no polling. Use it the moment you need that result to continue; never `sleep` and ' +
249
252
  'read the log by hand. If it is still running when the budget ends, the result says so ' +
250
253
  'and you can call it again. A suite whose failures keep repeating one cause comes back ' +
251
- 'EARLY with that cause named, the tests it hit and the command that stops the run — act ' +
252
- 'on it instead of waiting for the rest.',
254
+ 'EARLY with that cause named and the tests it hit — start fixing it, and still LET THE RUN ' +
255
+ 'FINISH, because the run is what names its other causes; the note says so, and names the ' +
256
+ 'command that stops the run only when that one cause is failing every test. A finished ' +
257
+ 'run with failures comes back with a note grouping them by cause, so one round of fixes ' +
258
+ 'covers every cause it found.',
253
259
  parameters: {
254
260
  type: 'object',
255
261
  properties: {
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkL9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAoDnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAuzDxF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkL9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA0DnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAk4DxF"}