@molecule/api-ai-tools 1.8.2 → 1.8.3

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.
@@ -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,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,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,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,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"}
package/dist/index.js CHANGED
@@ -137,6 +137,7 @@ export * from './cut-run.js';
137
137
  export * from './input-normalizer.js';
138
138
  export * from './no-test-run.js';
139
139
  export * from './schemas.js';
140
+ export * from './sleep-peek.js';
140
141
  export * from './suite-overlap.js';
141
142
  export * from './system-prompt.js';
142
143
  export * from './tools.js';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A leading `sleep` in an executor's command: the seconds it would have waited, and the command
3
+ * that follows it.
4
+ *
5
+ * X1 xr-molecule-flash-m105 (+08:49 → +10:53, 2026-10-17) spent 225 s of its 60-minute clock
6
+ * inside `exec_command` calls of the shape `sleep N; tail <task log>`. The platform's own launch
7
+ * note said "peek with exec_command: tail <log>" beside "never sleep", and the only concrete
8
+ * instruction was the peek, so the executor built the poll by hand. Two of its three sleeps were
9
+ * provably dead — the task had ended 0.1 s in (`playwright: not found`, exit 127) and another had
10
+ * already printed `Error: No tests found` — and the third spent 120 s asleep beside a live
11
+ * 139-test run whose failures were already in the log.
12
+ *
13
+ * The platform does not have to answer "what is the task doing" from a sleep: `wait_for_task`
14
+ * already returns the moment the task ends, early on a proven repeating cause, and with the
15
+ * progress and failures the log holds. This module answers only the parsing question — is the
16
+ * command's FIRST statement a `sleep` of {@link MIN_PEEK_SLEEP_SECONDS} seconds or more, and what
17
+ * follows it — from the package's own shell parse ({@link locatedCommandSegments}), so a `sleep`
18
+ * named in an argument (`grep -n 'sleep 60' notes.md`) or written after another statement
19
+ * (`cd x && sleep 60`) is not one.
20
+ *
21
+ * @module
22
+ */
23
+ /**
24
+ * The shortest leading sleep the platform drops: below it the wall clock it costs is noise, and a
25
+ * short settle (`sleep 2; curl …`) is a legitimate thing to run.
26
+ */
27
+ export declare const MIN_PEEK_SLEEP_SECONDS = 5;
28
+ /** The leading `sleep` a command opens with, and the command that follows it. */
29
+ export interface LeadingSleep {
30
+ /** How many seconds the statement would have slept (GNU sleep sums its operands). */
31
+ seconds: number;
32
+ /** What follows the leading statement, trimmed — '' when the sleep was the whole command. */
33
+ rest: string;
34
+ }
35
+ /**
36
+ * The leading `sleep` of a command, when the first statement is one and it asks for at least
37
+ * {@link MIN_PEEK_SLEEP_SECONDS}.
38
+ *
39
+ * The statement is read by {@link locatedCommandSegments} — the same parse every other command
40
+ * classifier uses — so only a `sleep` in command position of the FIRST statement counts. The end
41
+ * of that statement is then found by walking forward from its own start: the statement is
42
+ * `sleep <duration>`, whose words carry no quotes and open no substitution, so the first unquoted
43
+ * `;`, newline or `&` ends it. A `|` is refused: dropping the left side of a pipeline would hand
44
+ * the right side its stdin instead, and a `||` fallback would run on a success that never did.
45
+ *
46
+ * @param command - The shell command the executor sent.
47
+ * @returns The seconds and the remaining command, or null when the command does not open with a
48
+ * long `sleep`.
49
+ */
50
+ export declare function leadingSleepPeek(command: string): LeadingSleep | null;
51
+ //# sourceMappingURL=sleep-peek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sleep-peek.d.ts","sourceRoot":"","sources":["../src/sleep-peek.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAA;IACf,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAyBrE"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A leading `sleep` in an executor's command: the seconds it would have waited, and the command
3
+ * that follows it.
4
+ *
5
+ * X1 xr-molecule-flash-m105 (+08:49 → +10:53, 2026-10-17) spent 225 s of its 60-minute clock
6
+ * inside `exec_command` calls of the shape `sleep N; tail <task log>`. The platform's own launch
7
+ * note said "peek with exec_command: tail <log>" beside "never sleep", and the only concrete
8
+ * instruction was the peek, so the executor built the poll by hand. Two of its three sleeps were
9
+ * provably dead — the task had ended 0.1 s in (`playwright: not found`, exit 127) and another had
10
+ * already printed `Error: No tests found` — and the third spent 120 s asleep beside a live
11
+ * 139-test run whose failures were already in the log.
12
+ *
13
+ * The platform does not have to answer "what is the task doing" from a sleep: `wait_for_task`
14
+ * already returns the moment the task ends, early on a proven repeating cause, and with the
15
+ * progress and failures the log holds. This module answers only the parsing question — is the
16
+ * command's FIRST statement a `sleep` of {@link MIN_PEEK_SLEEP_SECONDS} seconds or more, and what
17
+ * follows it — from the package's own shell parse ({@link locatedCommandSegments}), so a `sleep`
18
+ * named in an argument (`grep -n 'sleep 60' notes.md`) or written after another statement
19
+ * (`cd x && sleep 60`) is not one.
20
+ *
21
+ * @module
22
+ */
23
+ import { durationSeconds, locatedCommandSegments } from './command-position.js';
24
+ /**
25
+ * The shortest leading sleep the platform drops: below it the wall clock it costs is noise, and a
26
+ * short settle (`sleep 2; curl …`) is a legitimate thing to run.
27
+ */
28
+ export const MIN_PEEK_SLEEP_SECONDS = 5;
29
+ /**
30
+ * The leading `sleep` of a command, when the first statement is one and it asks for at least
31
+ * {@link MIN_PEEK_SLEEP_SECONDS}.
32
+ *
33
+ * The statement is read by {@link locatedCommandSegments} — the same parse every other command
34
+ * classifier uses — so only a `sleep` in command position of the FIRST statement counts. The end
35
+ * of that statement is then found by walking forward from its own start: the statement is
36
+ * `sleep <duration>`, whose words carry no quotes and open no substitution, so the first unquoted
37
+ * `;`, newline or `&` ends it. A `|` is refused: dropping the left side of a pipeline would hand
38
+ * the right side its stdin instead, and a `||` fallback would run on a success that never did.
39
+ *
40
+ * @param command - The shell command the executor sent.
41
+ * @returns The seconds and the remaining command, or null when the command does not open with a
42
+ * long `sleep`.
43
+ */
44
+ export function leadingSleepPeek(command) {
45
+ if (typeof command !== 'string')
46
+ return null;
47
+ const first = locatedCommandSegments(command)[0];
48
+ if (!first || first.start === null || first.segment === null)
49
+ return null;
50
+ if (first.segment.program !== 'sleep')
51
+ return null;
52
+ // GNU sleep sums its operands (`sleep 45 15`); every one of them has to be a duration.
53
+ let seconds = 0;
54
+ for (const arg of first.segment.args) {
55
+ const part = durationSeconds(arg);
56
+ if (part === null)
57
+ return null;
58
+ seconds += part;
59
+ }
60
+ if (seconds < MIN_PEEK_SLEEP_SECONDS)
61
+ return null;
62
+ let end = command.length;
63
+ for (let i = first.start; i < command.length; i++) {
64
+ const char = command[i] ?? '';
65
+ if (char === '|')
66
+ return null;
67
+ if (char === ';' || char === '\n' || char === '&') {
68
+ end = i;
69
+ break;
70
+ }
71
+ }
72
+ let at = end;
73
+ while (at < command.length && /[;&\s]/.test(command[at] ?? ''))
74
+ at += 1;
75
+ return { seconds, rest: command.slice(at).trim() };
76
+ }
package/dist/tools.d.ts CHANGED
@@ -60,8 +60,47 @@ export declare const SUITE_OVER_BUDGET_ADVICE: string;
60
60
  */
61
61
  export declare function moleculeDistReadNote(path: string): string | null;
62
62
  /**
63
- * The refusal for a write whose target is inside an installed package, or null
64
- * when the path is not under a `node_modules` directory.
63
+ * What the filesystem says about a write target under `node_modules` — the half
64
+ * of the decision a path's spelling cannot make.
65
+ *
66
+ * @remarks
67
+ * A path that reads `node_modules` is not necessarily an install: a run links
68
+ * `node_modules` beside its e2e copy to borrow the hoisted tree, and that link
69
+ * is its own. X0 xr-molecule-flash-m106 (+07:58) made exactly that link, found
70
+ * it dangling (everything was hoisted to `/workspace/node_modules`), and was
71
+ * refused three times for trying to remove it — 40 s and a "3rd time this turn
72
+ * … ask the user" escalation, in an unattended run, over a broken symlink in
73
+ * the run's own scratch directory. So the guard asks what is there.
74
+ */
75
+ export interface InstalledPackageFacts {
76
+ /** Whether the `node_modules` entry the path goes through is a symbolic link. */
77
+ readonly symlink: boolean;
78
+ /** Whether a package is really installed there: a `package.json` resolves under the entry. */
79
+ readonly installed: boolean;
80
+ }
81
+ /**
82
+ * Looks at the filesystem for {@link InstalledPackageFacts}.
83
+ *
84
+ * The guard cannot `stat` the path itself: the tools act through a backend (a
85
+ * sandbox over the network for the fleet), so the caller — which holds that
86
+ * backend — supplies the look. {@link shellInstalledPackageProbe} builds one
87
+ * from any shell runner.
88
+ */
89
+ export type InstalledPackageProbe = (path: string) => Promise<InstalledPackageFacts>;
90
+ /**
91
+ * Build an {@link InstalledPackageProbe} from a shell runner (a backend's
92
+ * `run`, or a sandbox's `exec`): one command reports whether the entry is a
93
+ * link and whether a `package.json` resolves under it.
94
+ *
95
+ * @param run - Runs a command where the path lives.
96
+ * @returns The probe.
97
+ */
98
+ export declare function shellInstalledPackageProbe(run: (command: string) => Promise<{
99
+ stdout?: string;
100
+ } | null | undefined>): InstalledPackageProbe;
101
+ /**
102
+ * The refusal for a write whose target is inside an INSTALLED package, or null
103
+ * when the path is not under `node_modules` or nothing is installed there.
65
104
  *
66
105
  * `node_modules` is not the project's code: nothing written there survives the
67
106
  * next install, no deploy carries it, and a sandbox whose dependency tree is
@@ -74,6 +113,19 @@ export declare function moleculeDistReadNote(path: string): string | null;
74
113
  * putting the file back. Reading these files stays allowed (that is how the
75
114
  * declarations above are read at all).
76
115
  *
116
+ * What is protected is the INSTALL, not the spelling: the decision comes from
117
+ * {@link InstalledPackageFacts}, never from the path alone (m106 above). Three
118
+ * things are the run's own to remove or replace —
119
+ *
120
+ * - a `node_modules` that is a SYMLINK: `rm`/`unlink` removes the link, never the
121
+ * tree it points at, so a link the run made cannot clobber anything;
122
+ * - anything under the run's own `.mol-scratch/` directory;
123
+ * - a `node_modules` with no `package.json` under it (nothing was ever installed).
124
+ *
125
+ * A path INSIDE the link is still decided by what resolves there: a link into a
126
+ * real tree is refused, because removing or writing through it reaches the
127
+ * install.
128
+ *
77
129
  * The restore route is named because the model that has ALREADY clobbered a file
78
130
  * (this refusal is too late for it) is the one who needs it, and because the
79
131
  * obvious repair — `npm install <pkg>` — is itself refused in the pre-installed
@@ -81,9 +133,10 @@ export declare function moleculeDistReadNote(path: string): string | null;
81
133
  * m91 used, and what both guards leave alone.
82
134
  *
83
135
  * @param path - An absolute path a tool is about to write.
136
+ * @param probe - Looks at the filesystem where the path lives.
84
137
  * @returns The one-line refusal naming the package, or null when nothing is installed there.
85
138
  */
86
- export declare function installedPackageWriteRefusal(path: string): string | null;
139
+ export declare function installedPackageWriteRefusal(path: string, probe?: InstalledPackageProbe): Promise<string | null>;
87
140
  /**
88
141
  * Build a complete set of AI agent tools bound to an execution backend.
89
142
  *
@@ -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;AA0G9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAyHD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAkEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBxE;AAgDD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAyzExF"}
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;AAgJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAiOD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAmEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;AAoBpF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC,GACxE,qBAAqB,CAkBvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AAgDD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAu2ExF"}