@molecule/api-ai-tools 1.9.16 → 1.9.19

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.
@@ -68,6 +68,75 @@ const LAUNCHERS = new Set([
68
68
  ]);
69
69
  /** The shells whose `-c '<command>'` argument is a command of its own. */
70
70
  const SHELLS = new Set(['ash', 'bash', 'dash', 'ksh', 'sh', 'zsh']);
71
+ /**
72
+ * Node, which is a wrapper only when the script it runs IS a tool's own CLI — never in general:
73
+ * `node script.mjs` RUNS that script, so the parser's answer for it stays `node`.
74
+ *
75
+ * The Playwright runner is spelled this way whenever a project reaches it by file instead of by
76
+ * the `.bin` shim: xr-molecule-flash-m179 (+17:51) ran
77
+ * `node /workspace/node_modules/.bin/playwright test` after `npx playwright test` in every
78
+ * earlier call, and no reader recognised it — the run got no `--trace`, no suite-budget pin and
79
+ * no proof credit, and the platform spent the rest of the turn re-walking a suite it had already
80
+ * walked green.
81
+ */
82
+ const NODE = new Set(['node']);
83
+ /**
84
+ * The Playwright CLI, as the path operand of a `node` invocation: the package's `.bin` shim, or
85
+ * the `cli.js` a package installs (`playwright/cli.js`, `@playwright/test/cli.js`).
86
+ */
87
+ const PLAYWRIGHT_CLI = /(?:^|\/)node_modules\/(?:\.bin\/playwright|(?:playwright|@playwright\/test)\/cli\.(?:js|mjs|cjs))$/;
88
+ /**
89
+ * The runner a `node` script operand IS, or null when the operand is any other script.
90
+ *
91
+ * @param word - The script operand, as written.
92
+ * @returns The runner's name (`playwright`), or null.
93
+ */
94
+ function nodeScriptRunner(word) {
95
+ return PLAYWRIGHT_CLI.test(unquoted(word)) ? 'playwright' : null;
96
+ }
97
+ /**
98
+ * Node's own value-taking options: the ones that consume the word after them, so the script
99
+ * operand is not mistaken for a value (`node -r tsx/cli.js …`). Node also accepts `--flag=value`.
100
+ */
101
+ const NODE_VALUE_FLAGS = new Set([
102
+ '-e',
103
+ '--eval',
104
+ '-p',
105
+ '--print',
106
+ '-r',
107
+ '--require',
108
+ '--import',
109
+ '--loader',
110
+ '--experimental-loader',
111
+ '--input-type',
112
+ '--conditions',
113
+ '--title',
114
+ ]);
115
+ /**
116
+ * The word a `node` invocation runs as its script: the first operand after node's own options,
117
+ * with `--` ending the options. Null when node runs no file (`node --test`, `node -e '<code>'`).
118
+ *
119
+ * A wrong guess here is harmless: the operand is then checked against {@link PLAYWRIGHT_CLI}, so
120
+ * an ordinary script (`node probe.mjs`) is never read as a runner.
121
+ *
122
+ * @param words - The segment's words.
123
+ * @param from - The index just after `node`.
124
+ * @returns The script operand's index, or null.
125
+ */
126
+ function nodeScriptIndex(words, from) {
127
+ let at = from;
128
+ for (let guard = 0; guard < MAX_WRAPPERS && at < words.length; guard++) {
129
+ const word = words[at] ?? '';
130
+ if (!word.startsWith('-'))
131
+ return at;
132
+ const flag = unquoted(word);
133
+ // `--` ends node's options; the script is the next word.
134
+ if (flag === '--')
135
+ return at + 1 < words.length ? at + 1 : null;
136
+ at += flag.includes('=') || !NODE_VALUE_FLAGS.has(flag) ? 1 : 2;
137
+ }
138
+ return null;
139
+ }
71
140
  /** Package runners that run the program named after them (`npx playwright test`). */
72
141
  const PACKAGE_RUNNERS = new Set(['bunx', 'npx', 'pnpx']);
73
142
  /** Package managers whose sub-command runs a package's binary (`bun x`, `pnpm dlx`, `npm exec`). */
@@ -345,6 +414,8 @@ function nestedCommand(words, from) {
345
414
  * @returns The index of the word in command position (one past the end when the segment runs
346
415
  * nothing), the command string of a `sh -c '<command>'` this segment turns out to be, and the
347
416
  * seconds its own `timeout` wrapper allows that command (`timeout 280 hurl …` → 280) or null.
417
+ * `program` names what runs when that is not the word itself's name — `node <path>/playwright`
418
+ * is a `playwright` invocation, not a `node` one ({@link nodeScriptRunner}).
348
419
  */
349
420
  function commandStart(words) {
350
421
  let at = 0;
@@ -364,14 +435,14 @@ function commandStart(words) {
364
435
  if (SHELLS.has(name)) {
365
436
  const nested = nestedCommand(words, at + 1);
366
437
  if (nested !== null)
367
- return { at, nested, timeoutSeconds };
438
+ return { at, nested, timeoutSeconds, program: null };
368
439
  at += 1 + flagWords(words, at + 1, name);
369
440
  continue;
370
441
  }
371
442
  if (PACKAGE_RUNNERS.has(name)) {
372
443
  const runner = packageRunnerOptions(words, at + 1, name);
373
444
  if (runner.nested !== null)
374
- return { at, nested: runner.nested, timeoutSeconds };
445
+ return { at, nested: runner.nested, timeoutSeconds, program: null };
375
446
  at += 1 + runner.count;
376
447
  continue;
377
448
  }
@@ -380,19 +451,35 @@ function commandStart(words) {
380
451
  const subcommand = unquoted(words[at + 1] ?? '');
381
452
  const runner = packageRunnerOptions(words, at + 2, `${name} ${subcommand}`);
382
453
  if (runner.nested !== null)
383
- return { at, nested: runner.nested, timeoutSeconds };
454
+ return { at, nested: runner.nested, timeoutSeconds, program: null };
384
455
  at += 2 + runner.count;
385
456
  continue;
386
457
  }
458
+ if (name === 'yarn' && unquoted(words[at + 1] ?? '') === 'playwright') {
459
+ // Yarn classic runs the binary named after it (`yarn playwright test`); Yarn 2+ spells the
460
+ // same thing `yarn exec`/`yarn dlx` ({@link PACKAGE_RUNNER_SUBCOMMANDS}). Only the runner's
461
+ // own name is read through it — `yarn test` is the project's own script, not a binary.
462
+ at += 1;
463
+ continue;
464
+ }
465
+ if (NODE.has(name)) {
466
+ // `node <path>/playwright test` is a Playwright invocation; `node probe.mjs` runs the
467
+ // project's own script and stays a `node` statement (`program: null`).
468
+ const script = nodeScriptIndex(words, at + 1);
469
+ const runner = script === null ? null : nodeScriptRunner(words[script] ?? '');
470
+ return runner === null
471
+ ? { at, nested: null, timeoutSeconds, program: null }
472
+ : { at: script ?? at, nested: null, timeoutSeconds, program: runner };
473
+ }
387
474
  if (!LAUNCHERS.has(name))
388
- return { at, nested: null, timeoutSeconds };
475
+ return { at, nested: null, timeoutSeconds, program: null };
389
476
  at += 1 + flagWords(words, at + 1, name);
390
477
  if (name === 'timeout' && DURATION.test(words[at] ?? '')) {
391
478
  timeoutSeconds = durationSeconds(words[at] ?? '');
392
479
  at += 1;
393
480
  }
394
481
  }
395
- return { at, nested: null, timeoutSeconds };
482
+ return { at, nested: null, timeoutSeconds, program: null };
396
483
  }
397
484
  /**
398
485
  * A command with every heredoc BODY blanked to spaces, so its length and every offset in it are
@@ -993,7 +1080,9 @@ function locatedSegmentsOf(command, depth) {
993
1080
  assignmentOffset: part.start + leading.end,
994
1081
  programOffset: words[start.at] === undefined ? null : part.start + (wordOffset(part.text, start.at) ?? 0),
995
1082
  };
996
- const program = words[start.at] === undefined ? '' : programName(words[start.at] ?? '');
1083
+ // The runner the word RUNS names, when that is not the word's own name (`node <path>/playwright`
1084
+ // is a `playwright` invocation, xr-molecule-flash-m179).
1085
+ const program = words[start.at] === undefined ? '' : (start.program ?? programName(words[start.at] ?? ''));
997
1086
  const args = words.slice(start.at + 1);
998
1087
  const redirectTarget = redirectTargetOf(part.text, part.depths, program, args);
999
1088
  if (start.nested !== null) {
package/dist/index.d.ts CHANGED
@@ -141,6 +141,7 @@ export * from './no-test-run.js';
141
141
  export * from './output-filter.js';
142
142
  export * from './pipeline-exit-note.js';
143
143
  export * from './schemas.js';
144
+ export * from './server-command.js';
144
145
  export * from './sleep-peek.js';
145
146
  export * from './suite-overlap.js';
146
147
  export * from './system-prompt.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,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,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
@@ -141,6 +141,7 @@ export * from './no-test-run.js';
141
141
  export * from './output-filter.js';
142
142
  export * from './pipeline-exit-note.js';
143
143
  export * from './schemas.js';
144
+ export * from './server-command.js';
144
145
  export * from './sleep-peek.js';
145
146
  export * from './suite-overlap.js';
146
147
  export * from './system-prompt.js';
@@ -19,9 +19,13 @@
19
19
  import { type CommandSegment } from './command-position.js';
20
20
  /** A cutting stage that follows the runner, and the command with the whole pipe removed. */
21
21
  export interface RunnerOutputFilter {
22
- /** The stage exactly as the runner's command wrote it — `tail -6`. */
22
+ /**
23
+ * The stage exactly as the runner's command wrote it — `tail -6`. When several cutting filters
24
+ * follow the runner, this is the whole trailing chain as written, pipes included:
25
+ * `grep -E "Failure|actual" | head -40`.
26
+ */
23
27
  stage: string;
24
- /** The pipe as a sentence names it — `| tail -6`. */
28
+ /** The pipe as a sentence names it — `| tail -6`, or the whole chain `| grep … | head -40`. */
25
29
  pipe: string;
26
30
  /**
27
31
  * The command with the pipe and the stage removed — `hurl --variable uid=… --test /tmp/t.hurl
@@ -33,16 +37,23 @@ export interface RunnerOutputFilter {
33
37
  /**
34
38
  * The output filter that follows the runner in `command`, or null when there is none.
35
39
  *
36
- * Only a stage of the runner's OWN pipeline counts (its separator is `|`), and only when it is the
37
- * LAST stage of that pipeline: a `| grep … | wc -l` chain would leave a dangling pipe if the
38
- * middle stage were cut out, so a chain is not answered here at all. A filter before the runner
39
- * (`cat run.log | tail -20 && hurl …`) reads something else's output and is not this.
40
+ * Only stages of the runner's OWN pipeline count (their separator is `|`), and they are read as
41
+ * one trailing CHAIN: `| grep … | head -40` is two cuts, and removing only the first would leave
42
+ * the run piped into the second. Every stage after the runner must be a cutting filter and the
43
+ * chain must run to the pipeline's end — a pipeline that continues into `wc -l` is not answered
44
+ * here, because removing the cutting stages alone would still leave the failure hidden. A filter
45
+ * before the runner (`cat run.log | tail -20 && hurl …`) reads something else's output and is not
46
+ * this.
40
47
  *
41
48
  * @param command - The command as the executor sent it.
42
49
  * @param runs - Whether a segment is the runner this run is about (the caller knows its own kind:
43
50
  * {@link CommandSegment.program} is the program, once wrapper words like `timeout 280` and
44
51
  * `npx` are taken away).
52
+ * @param occurrence - Which runner invocation the output belongs to, 1-based. A command can run
53
+ * the same runner more than once (xr-molecule-flash-m170: the suite bare, then with `HOST=…`),
54
+ * and each invocation carries its OWN pipe; a reader judging the second walk must name the
55
+ * second walk's cut, never the first's.
45
56
  * @returns The filter and the unfiltered command, or null.
46
57
  */
47
- export declare function outputFilterAfterRunner(command: string, runs: (segment: CommandSegment) => boolean): RunnerOutputFilter | null;
58
+ export declare function outputFilterAfterRunner(command: string, runs: (segment: CommandSegment) => boolean, occurrence?: number): RunnerOutputFilter | null;
48
59
  //# sourceMappingURL=output-filter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"output-filter.d.ts","sourceRoot":"","sources":["../src/output-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,uBAAuB,CAAA;AASnF,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,GACzC,kBAAkB,GAAG,IAAI,CAkB3B"}
1
+ {"version":3,"file":"output-filter.d.ts","sourceRoot":"","sources":["../src/output-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,uBAAuB,CAAA;AAS9B,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,EAC1C,UAAU,SAAI,GACb,kBAAkB,GAAG,IAAI,CAmC3B"}
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * @module
18
18
  */
19
- import { locatedCommandSegments } from './command-position.js';
19
+ import { locatedCommandSegments, } from './command-position.js';
20
20
  /**
21
21
  * The programs whose job is to re-shape what an earlier stage printed and that can DROP lines —
22
22
  * the reader's own cut, never part of the run. `tee`, `sort` and `wc` are output plumbing too, but
@@ -26,39 +26,67 @@ const CUTTING_FILTERS = new Set(['awk', 'cut', 'grep', 'head', 'sed', 'tail']);
26
26
  /**
27
27
  * The output filter that follows the runner in `command`, or null when there is none.
28
28
  *
29
- * Only a stage of the runner's OWN pipeline counts (its separator is `|`), and only when it is the
30
- * LAST stage of that pipeline: a `| grep … | wc -l` chain would leave a dangling pipe if the
31
- * middle stage were cut out, so a chain is not answered here at all. A filter before the runner
32
- * (`cat run.log | tail -20 && hurl …`) reads something else's output and is not this.
29
+ * Only stages of the runner's OWN pipeline count (their separator is `|`), and they are read as
30
+ * one trailing CHAIN: `| grep … | head -40` is two cuts, and removing only the first would leave
31
+ * the run piped into the second. Every stage after the runner must be a cutting filter and the
32
+ * chain must run to the pipeline's end — a pipeline that continues into `wc -l` is not answered
33
+ * here, because removing the cutting stages alone would still leave the failure hidden. A filter
34
+ * before the runner (`cat run.log | tail -20 && hurl …`) reads something else's output and is not
35
+ * this.
33
36
  *
34
37
  * @param command - The command as the executor sent it.
35
38
  * @param runs - Whether a segment is the runner this run is about (the caller knows its own kind:
36
39
  * {@link CommandSegment.program} is the program, once wrapper words like `timeout 280` and
37
40
  * `npx` are taken away).
41
+ * @param occurrence - Which runner invocation the output belongs to, 1-based. A command can run
42
+ * the same runner more than once (xr-molecule-flash-m170: the suite bare, then with `HOST=…`),
43
+ * and each invocation carries its OWN pipe; a reader judging the second walk must name the
44
+ * second walk's cut, never the first's.
38
45
  * @returns The filter and the unfiltered command, or null.
39
46
  */
40
- export function outputFilterAfterRunner(command, runs) {
47
+ export function outputFilterAfterRunner(command, runs, occurrence = 1) {
41
48
  if (typeof command !== 'string' || command.trim() === '')
42
49
  return null;
43
50
  const entries = locatedCommandSegments(command);
44
- const runnerAt = entries.findIndex((entry) => entry.segment !== null && runs(entry.segment));
45
- if (runnerAt < 0 || runnerAt + 1 >= entries.length)
51
+ const runners = [];
52
+ entries.forEach((entry, index) => {
53
+ if (entry.segment !== null && runs(entry.segment))
54
+ runners.push(index);
55
+ });
56
+ const runnerAt = runners[occurrence - 1];
57
+ if (runnerAt === undefined)
46
58
  return null;
47
59
  const runner = entries[runnerAt];
48
- const filter = entries[runnerAt + 1];
49
- // The stage must be fed by the runner's own pipe, and nothing may be piped out of the filter:
50
- // those are the two positions where cutting it out leaves a command that still runs the run.
51
- if (!runner || !filter || filter.separator.trim() !== '|')
60
+ if (!runner || runner.end === null)
52
61
  return null;
53
- if (!filter.segment || !CUTTING_FILTERS.has(filter.segment.program))
62
+ // The chain is every stage fed by the runner's own pipe, until the pipeline ends. Each one has
63
+ // to be a cutting filter: a stage that does not drop lines ends the chain, and the chain then
64
+ // stops before the pipeline's end — which is not answerable (`| grep … | wc -l` would still
65
+ // hide the assertion in a count).
66
+ const chain = [];
67
+ for (let index = runnerAt + 1; index < entries.length; index++) {
68
+ const entry = entries[index];
69
+ if (!entry || entry.separator.trim() !== '|')
70
+ break;
71
+ if (!entry.segment || !CUTTING_FILTERS.has(entry.segment.program))
72
+ break;
73
+ chain.push(entry);
74
+ if (entry.terminator.trim() !== '|')
75
+ break;
76
+ }
77
+ const first = chain[0];
78
+ const last = chain[chain.length - 1];
79
+ if (!first || !last)
54
80
  return null;
55
- if (filter.terminator.trim() === '|')
81
+ // A pipeline that continues past the chain means a non-cutting stage sits after it.
82
+ if (last.terminator.trim() === '|')
56
83
  return null;
57
- if (runner.end === null || filter.start === null || filter.end === null)
84
+ if (first.start === null || last.end === null)
58
85
  return null;
86
+ const stage = command.slice(first.start, last.end);
59
87
  return {
60
- stage: command.slice(filter.start, filter.end),
61
- pipe: `| ${command.slice(filter.start, filter.end)}`,
62
- command: `${command.slice(0, runner.end)}${command.slice(filter.end)}`,
88
+ stage,
89
+ pipe: `| ${stage}`,
90
+ command: `${command.slice(0, runner.end)}${command.slice(last.end)}`,
63
91
  };
64
92
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * "This command starts a server, and a server never exits" — ONE reading of a server launch,
3
+ * shared by the `exec_command` refusal, the over-budget advice, and the platform's own-server note
4
+ * (`molecule-dev/api/src/ai/own-server-note.ts` imports {@link SERVER_PROGRAM} and
5
+ * {@link BACKGROUNDED} from here), so every surface that names a server names the same commands.
6
+ *
7
+ * X1 xr-molecule-flash-m167 (+19:16 → +24:04) wrote `/tmp/probe.mts` whose body imports the app's
8
+ * `api/src/server.ts` and calls its `create(0)`, then ran it in the FOREGROUND:
9
+ * `npx tsx --env-file=.env /tmp/probe.mts 2>&1 | tail -15`. The scaffold's `create()` calls
10
+ * `server.listen(port)` and returns the server, so the process never exits — and behind `| tail`
11
+ * it printed nothing until it did. The tool waited its whole 290 s budget and answered with "run
12
+ * a smaller unit", advice about the wrong axis for a process that was never going to return.
13
+ *
14
+ * A server started in the background is a different thing: it is the launch the platform records
15
+ * and hands a task id for, and the over-budget advice below keeps it off the foreground budget.
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * A background launch: a single `&` (not `&&`, not a `2>&1`) or an explicit detacher. A
21
+ * backgrounded launch never holds the tool, so every reading here stops at it.
22
+ */
23
+ export declare const BACKGROUNDED: RegExp;
24
+ /**
25
+ * A program that SERVES: the scaffold's entry (`server.ts`/`server.js`), a package manager's
26
+ * `start`/`dev`/`serve`/`preview` script, a Vite dev or preview server, `next start|dev`, or a
27
+ * Python WSGI server. Matched against a command segment's own text, and against a script's SOURCE
28
+ * when the command only names the script file.
29
+ */
30
+ export declare const SERVER_PROGRAM: RegExp;
31
+ /**
32
+ * Whether a command, read from its TEXT alone, starts a server that stays up in the foreground.
33
+ *
34
+ * The over-budget reader's shape: at that point the command has already run, and only its text is
35
+ * left to say whether "run a smaller unit" was ever the right advice.
36
+ *
37
+ * @param command - The command as the executor sent it.
38
+ * @returns True when a foreground statement of the command starts a server.
39
+ */
40
+ export declare function startsServerProgram(command: string): boolean;
41
+ /** A foreground command that starts a server, as the refusal names it. */
42
+ export interface ForegroundServer {
43
+ /** The program (and script, when there is one) that serves. */
44
+ readonly program: string;
45
+ }
46
+ /**
47
+ * A FOREGROUND command that starts a server, including one that only names a script whose SOURCE
48
+ * imports the app's server and calls its `create()`/`listen()`.
49
+ *
50
+ * The script read is best-effort: a path the backend will not answer for is a command this reading
51
+ * cannot classify, never a refusal. A backgrounded launch, a test runner and a test file are all
52
+ * left alone.
53
+ *
54
+ * @param command - The command as the executor sent it.
55
+ * @param readScript - Reads a script file the command names (`backend.readFile`).
56
+ * @returns The serving program, or null when the command starts no server in the foreground.
57
+ */
58
+ export declare function foregroundServerCommand(command: string, readScript: (path: string) => Promise<string>): Promise<ForegroundServer | null>;
59
+ /**
60
+ * The refusal for a foreground command that starts a server: why it would never return, and the
61
+ * two shapes that do work — a background launch, or a probe of the API already running.
62
+ *
63
+ * @param server - The serving program, from {@link foregroundServerCommand}.
64
+ * @param budgetSeconds - The tool's budget, which a foreground server would consume whole.
65
+ * @returns The refusal sentence.
66
+ */
67
+ export declare function serverCommandRefusal(server: ForegroundServer, budgetSeconds: number): string;
68
+ /**
69
+ * The closing advice on an over-budget message for a command recognised as a server start: there
70
+ * is no tally to split, and the run has to leave the foreground budget to keep serving.
71
+ */
72
+ export declare const SERVER_OVER_BUDGET_ADVICE: string;
73
+ //# sourceMappingURL=server-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-command.d.ts","sourceRoot":"","sources":["../src/server-command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH;;;GAGG;AACH,eAAO,MAAM,YAAY,QAA6C,CAAA;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QACmP,CAAA;AAmF9Q;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAI5D;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAC5C,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAmBlC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAO5F;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,QAG0B,CAAA"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * "This command starts a server, and a server never exits" — ONE reading of a server launch,
3
+ * shared by the `exec_command` refusal, the over-budget advice, and the platform's own-server note
4
+ * (`molecule-dev/api/src/ai/own-server-note.ts` imports {@link SERVER_PROGRAM} and
5
+ * {@link BACKGROUNDED} from here), so every surface that names a server names the same commands.
6
+ *
7
+ * X1 xr-molecule-flash-m167 (+19:16 → +24:04) wrote `/tmp/probe.mts` whose body imports the app's
8
+ * `api/src/server.ts` and calls its `create(0)`, then ran it in the FOREGROUND:
9
+ * `npx tsx --env-file=.env /tmp/probe.mts 2>&1 | tail -15`. The scaffold's `create()` calls
10
+ * `server.listen(port)` and returns the server, so the process never exits — and behind `| tail`
11
+ * it printed nothing until it did. The tool waited its whole 290 s budget and answered with "run
12
+ * a smaller unit", advice about the wrong axis for a process that was never going to return.
13
+ *
14
+ * A server started in the background is a different thing: it is the launch the platform records
15
+ * and hands a task id for, and the over-budget advice below keeps it off the foreground budget.
16
+ *
17
+ * @module
18
+ */
19
+ import { commandSegments } from './command-position.js';
20
+ import { withoutHeredocs } from './suite-overlap.js';
21
+ /**
22
+ * A background launch: a single `&` (not `&&`, not a `2>&1`) or an explicit detacher. A
23
+ * backgrounded launch never holds the tool, so every reading here stops at it.
24
+ */
25
+ export const BACKGROUNDED = /(?:^|[^&>])&(?!&|>)|\b(?:nohup|setsid)\b/;
26
+ /**
27
+ * A program that SERVES: the scaffold's entry (`server.ts`/`server.js`), a package manager's
28
+ * `start`/`dev`/`serve`/`preview` script, a Vite dev or preview server, `next start|dev`, or a
29
+ * Python WSGI server. Matched against a command segment's own text, and against a script's SOURCE
30
+ * when the command only names the script file.
31
+ */
32
+ export const SERVER_PROGRAM = /\bserver\.[cm]?[jt]s\b|\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:start|dev|serve|preview)\b|\bvite\s+(?:preview|dev|serve)\b|\bvite(?![.\w-])(?!\s+(?:build|optimize)\b)|\bnext\s+(?:start|dev)\b|\b(?:uvicorn|gunicorn)\b|\bflask\s+run\b|\bpython\d?\s+-m\s+http\.server\b/;
33
+ /** Programs that run a script FILE, so the file's own source decides whether it serves. */
34
+ const SCRIPT_RUNNERS = new Set([
35
+ 'bun',
36
+ 'deno',
37
+ 'node',
38
+ 'nodejs',
39
+ 'tsx',
40
+ 'ts-node',
41
+ ]);
42
+ /** Programs that serve by themselves, read against {@link SERVER_PROGRAM}. */
43
+ const SERVER_PROGRAMS = new Set([
44
+ 'bun',
45
+ 'deno',
46
+ 'flask',
47
+ 'gunicorn',
48
+ 'next',
49
+ 'node',
50
+ 'nodejs',
51
+ 'python',
52
+ 'python3',
53
+ 'tsx',
54
+ 'ts-node',
55
+ 'uvicorn',
56
+ 'vite',
57
+ ]);
58
+ /** A file a script runner executes. */
59
+ const SCRIPT_FILE = /\.(?:[cm]?[jt]s|tsx)$/;
60
+ /** A test file is a test, even when it imports the server to exercise it. */
61
+ const TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
62
+ /** Package-manager scripts that keep serving instead of finishing. */
63
+ const SERVE_SCRIPTS = /^(?:start|dev|serve|preview)$/;
64
+ /** Test runners: their command line names a suite, never a server to leave running. */
65
+ const TEST_RUNNERS = /^(?:hurl|jest|mocha|playwright|vitest)$/;
66
+ /** A script's SOURCE that binds a listener even when it does not name a server file. */
67
+ const STARTS_LISTENING = /\b(?:server|app)\.listen\s*\(/;
68
+ /** A word with its shell quotes taken off (`'src/server.ts'` → `src/server.ts`). */
69
+ function bare(word) {
70
+ const match = /^(['"])(.*)\1$/s.exec(word);
71
+ return match?.[2] ?? word;
72
+ }
73
+ /**
74
+ * Whether one parsed statement starts a server: a package manager's serving script, or a program
75
+ * that IS a server, named in command position (so `cat server.ts` is a read, not a launch).
76
+ *
77
+ * @param segment - One statement of the command.
78
+ * @returns True when the statement starts a server that does not exit.
79
+ */
80
+ function runsServerProgram(segment) {
81
+ if (TEST_RUNNERS.test(segment.program))
82
+ return false;
83
+ if (/^(?:npm|pnpm|yarn|bun)$/.test(segment.program)) {
84
+ const words = segment.args.map(bare);
85
+ const script = words[0] === 'run' ? words[1] : words[0];
86
+ if (script !== undefined && SERVE_SCRIPTS.test(script))
87
+ return true;
88
+ }
89
+ return SERVER_PROGRAMS.has(segment.program) && SERVER_PROGRAM.test(segment.text);
90
+ }
91
+ /**
92
+ * The script file a runner statement executes, when it names one.
93
+ *
94
+ * @param segment - One statement of the command.
95
+ * @returns The script's path as written, or null when the statement runs no script file.
96
+ */
97
+ function scriptPath(segment) {
98
+ if (!SCRIPT_RUNNERS.has(segment.program))
99
+ return null;
100
+ for (const arg of segment.args) {
101
+ const word = bare(arg);
102
+ if (word.startsWith('-'))
103
+ continue;
104
+ if (SCRIPT_FILE.test(word) && !TEST_FILE.test(word))
105
+ return word;
106
+ }
107
+ return null;
108
+ }
109
+ /**
110
+ * Whether a command, read from its TEXT alone, starts a server that stays up in the foreground.
111
+ *
112
+ * The over-budget reader's shape: at that point the command has already run, and only its text is
113
+ * left to say whether "run a smaller unit" was ever the right advice.
114
+ *
115
+ * @param command - The command as the executor sent it.
116
+ * @returns True when a foreground statement of the command starts a server.
117
+ */
118
+ export function startsServerProgram(command) {
119
+ const text = withoutHeredocs(command);
120
+ if (BACKGROUNDED.test(text))
121
+ return false;
122
+ return commandSegments(text).some((segment) => runsServerProgram(segment));
123
+ }
124
+ /**
125
+ * A FOREGROUND command that starts a server, including one that only names a script whose SOURCE
126
+ * imports the app's server and calls its `create()`/`listen()`.
127
+ *
128
+ * The script read is best-effort: a path the backend will not answer for is a command this reading
129
+ * cannot classify, never a refusal. A backgrounded launch, a test runner and a test file are all
130
+ * left alone.
131
+ *
132
+ * @param command - The command as the executor sent it.
133
+ * @param readScript - Reads a script file the command names (`backend.readFile`).
134
+ * @returns The serving program, or null when the command starts no server in the foreground.
135
+ */
136
+ export async function foregroundServerCommand(command, readScript) {
137
+ const text = withoutHeredocs(command);
138
+ if (BACKGROUNDED.test(text))
139
+ return null;
140
+ for (const segment of commandSegments(text)) {
141
+ if (runsServerProgram(segment))
142
+ return { program: segment.program };
143
+ const script = scriptPath(segment);
144
+ if (script === null)
145
+ continue;
146
+ let source;
147
+ try {
148
+ source = await readScript(script);
149
+ }
150
+ catch (_error) {
151
+ // A script the backend cannot read is not classified: the refusal must never be a guess.
152
+ continue;
153
+ }
154
+ if (SERVER_PROGRAM.test(source) || STARTS_LISTENING.test(source)) {
155
+ return { program: `${segment.program} ${script}` };
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+ /**
161
+ * The refusal for a foreground command that starts a server: why it would never return, and the
162
+ * two shapes that do work — a background launch, or a probe of the API already running.
163
+ *
164
+ * @param server - The serving program, from {@link foregroundServerCommand}.
165
+ * @param budgetSeconds - The tool's budget, which a foreground server would consume whole.
166
+ * @returns The refusal sentence.
167
+ */
168
+ export function serverCommandRefusal(server, budgetSeconds) {
169
+ return (`\`${server.program}\` starts a server, and a server never exits: run in the foreground it ` +
170
+ `would hold this tool for its whole ${budgetSeconds} s budget and return nothing. Start it ` +
171
+ 'with `run_in_background: true` and collect it with `wait_for_task`, or probe the API already ' +
172
+ 'running on :4000 with a command that finishes.');
173
+ }
174
+ /**
175
+ * The closing advice on an over-budget message for a command recognised as a server start: there
176
+ * is no tally to split, and the run has to leave the foreground budget to keep serving.
177
+ */
178
+ export const SERVER_OVER_BUDGET_ADVICE = 'A server is not a bigger unit to split: it never exits, so stopped here it reports nothing. ' +
179
+ 'Start it with `run_in_background: true` (it keeps serving and hands back a task id to ' +
180
+ '`wait_for_task`), or probe the API already running on :4000.';
@@ -83,6 +83,10 @@ export declare function withSuiteConfig(command: string, config: string | null |
83
83
  * suite, `--last-failed` dropped (it re-runs the tests that never ran too, X1 flash-m60) and
84
84
  * `--retries=0` set (a re-run of a known failure does not need to pay for retries).
85
85
  *
86
+ * The re-run keeps the run's `> <file>` redirect only in the sense of keeping its OUTPUT there:
87
+ * the target is rewritten to a fresh sibling ({@link redirectToFreshFile}), so a re-run never
88
+ * overwrites the log the run itself is the evidence for (xr-molecule-flash-m173).
89
+ *
86
90
  * `config` is the config the run was measured under ({@link withSuiteConfig}): the narrowed re-run
87
91
  * carries it whenever the command itself does not, so a run that dropped `--config` cannot be
88
92
  * handed back a command with the same defect (xr-molecule-flash-m113: every failing test was
@@ -139,6 +143,16 @@ export interface SuiteRunInFlight {
139
143
  export declare function suiteRunInTheWay<T extends SuiteRunInFlight>(candidate: {
140
144
  target: string;
141
145
  }, inFlight: readonly T[]): T | null;
146
+ /**
147
+ * What a caller knows about the run being judged beyond its command text.
148
+ */
149
+ export interface DiagnosisRunContext {
150
+ /**
151
+ * The test titles the person's request excludes by name — the same reading molecule-dev's
152
+ * `excludedTestTitles` gives the rest of the platform.
153
+ */
154
+ approvedExclusions?: readonly string[];
155
+ }
142
156
  /**
143
157
  * Whether a command walks a PART of a suite instead of all of it: a cap that stops it at a failure
144
158
  * count, the failures the last run printed (`--last-failed`, `--only-changed`), a title filter, or
@@ -166,8 +180,17 @@ export declare function suiteRunInTheWay<T extends SuiteRunInFlight>(candidate:
166
180
  * diagnosis. A command that starts no Playwright run is not one either (false): the question is
167
181
  * about a run already read as a Playwright suite.
168
182
  *
183
+ * A `--grep-invert` is only a narrowing when it excludes MORE than the request already excludes.
184
+ * When the caller passes those approved titles ({@link DiagnosisRunContext}) and the inverted
185
+ * pattern names one of them — the request's own "except …" scope written as the flag, which
186
+ * molecule-dev's `invertDecision` reads the same way — the run still walks the whole proof scope,
187
+ * and it is not a diagnosis. xr-molecule-flash-m170 (+33:16): the deployed proof carried exactly
188
+ * that exclusion, the rewriter added `--retries=0` to it, and the run lost the suite's retries on
189
+ * the one walk where a network blip is likeliest.
190
+ *
169
191
  * @param command - The command as the executor sent it.
192
+ * @param context - The request's approved exclusions, when the caller knows them.
170
193
  * @returns True when the run is a diagnosis or a re-run, not the whole suite.
171
194
  */
172
- export declare function isDiagnosisRun(command: string): boolean;
195
+ export declare function isDiagnosisRun(command: string, context?: DiagnosisRunContext): boolean;
173
196
  //# sourceMappingURL=suite-overlap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"suite-overlap.d.ts","sourceRoot":"","sources":["../src/suite-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoB1D;AAKD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAc1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,QAAQ,GAAE,SAAS,MAAM,EAAO,EAChC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EACtB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAClB,MAAM,CA0CR;AAgHD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,SAAS,EAAE,OAAO,CAAA;IAClB,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EACzD,SAAS,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EAC7B,QAAQ,EAAE,SAAS,CAAC,EAAE,GACrB,CAAC,GAAG,IAAI,CAMV;AAqDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQvD"}
1
+ {"version":3,"file":"suite-overlap.d.ts","sourceRoot":"","sources":["../src/suite-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoB1D;AAKD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAS1F;AAqCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,QAAQ,GAAE,SAAS,MAAM,EAAO,EAChC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EACtB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAClB,MAAM,CA8CR;AAkHD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,SAAS,EAAE,OAAO,CAAA;IAClB,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EACzD,SAAS,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EAC7B,QAAQ,EAAE,SAAS,CAAC,EAAE,GACrB,CAAC,GAAG,IAAI,CAMV;AA2FD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACvC;AAmCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAYtF"}