@molecule/api-ai-tools
Core interface · ai · API (Node) · v1.9.12 · Apache-2.0
Shared AI agent tools with backend abstraction for sandbox and local execution
npm install @molecule/api-ai-toolsHow it works
@molecule/api-ai-tools is the ai core interface on the API (Node) side: the API your app calls, with no vendor inside.
Bond a provider to choose the implementation.
import { buildAgentPrompt, buildTools, createLocalBackend } from '@molecule/api-ai-tools'
import { requireProvider } from '@molecule/api-ai'
const backend = createLocalBackend('/path/to/project')
const tools = buildTools(backend, { include: ['read_file', 'edit_file', 'search_files'] })
const system = buildAgentPrompt({
agentName: 'My Agent',
projectRoot: backend.projectRoot,
tools: tools.map((t) => t.name),
})
// Hand them to the bonded AI provider (or an @molecule/api-ai-agents run).
for await (const event of requireProvider().chat({ system, tools, messages, stream: true })) {
// forward text chunks; tool calls are executed against the backend
}Works with: @molecule/api-ai
Reference
Shared AI agent tool set for molecule.dev — filesystem, search, and shell tools an LLM agent can call, with a swappable execution backend.
buildTools(backend) returns ready-to-use AITools (the @molecule/api-ai
tool shape): list_files, read_file, write_file, edit_file,
search_files, find_files, create_directory, rename_file,
delete_file, exec_command, save_plan, load_skill. The backend decides
WHERE they act: createLocalBackend(projectRoot) (host filesystem) or
createSandboxBackend(...) (an isolated @molecule/api-code-sandbox
container). buildAgentPrompt(ctx) composes a matching system prompt
(identity, tool listing, project docs, discovered skills).
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { buildAgentPrompt, buildTools, createLocalBackend } from '@molecule/api-ai-tools'
import { requireProvider } from '@molecule/api-ai'
const backend = createLocalBackend('/path/to/project')
const tools = buildTools(backend, { include: ['read_file', 'edit_file', 'search_files'] })
const system = buildAgentPrompt({
agentName: 'My Agent',
projectRoot: backend.projectRoot,
tools: tools.map((t) => t.name),
})
// Hand them to the bonded AI provider (or an @molecule/api-ai-agents run).
for await (const event of requireProvider().chat({ system, tools, messages, stream: true })) {
// forward text chunks; tool calls are executed against the backend
}
Type
core
Installation
npm install @molecule/api-ai-tools @molecule/api-ai
API
Interfaces
CommandSegment
One statement or pipeline stage of a shell command: what it runs, and with what.
interface CommandSegment {
/**
* The word in command position, its directory removed (`/usr/bin/hurl` → `hurl`): the program
* the shell would run once whatever wrappers stood in front of it are taken away.
*/
program: string
/** The words after it, as written — quotes kept, so a quoted `"a|b"` is one word. */
args: readonly string[]
/** The program and its arguments as one line — what a runner pattern is matched against. */
text: string
/**
* The seconds the segment's own `timeout` wrapper allows the command it runs
* (`timeout 280 hurl --test …` → 280), or null when it carries none.
*/
timeoutSeconds: number | null
}
LocatedCommandSegment
One statement of a shell command, with where it sits and the variables it sets for itself.
commandSegments answers WHAT runs; this answers where, and what the shell binds to it — the
two things a caller that WRITES into a command needs. The platform composes a run from the
executor's own command in more than one place, and a value written for the runner belongs in
the runner's own statement: APP_BASE=https://… cd app && npx playwright test sets the variable
for cd alone, so the Playwright config falls back to the local app while the command line
still names the deployed host (xr-molecule-flash-m103 +28:08).
interface LocatedCommandSegment {
/**
* What the statement runs, or null when it only sets variables (`BASE=http://…` on its own):
* that shape persists in the shell, so a caller following assignments across `;`/`&&` has to
* see it.
*/
segment: CommandSegment | null
/** The statement's own leading `NAME=value` words, as written (`TEST_MODE=fullstack`). */
assignments: readonly string[]
/**
* The offset in the command of the statement's first word, or null for a segment inside a
* nested `sh -c '<command>'` string — its positions are offsets in THAT string, not in this
* command, so nothing may be spliced at them.
*/
start: number | null
/**
* The offset in the command just before the word the statement runs — after its own leading
* `NAME=value` words and their whitespace. A `NAME=value` written here is in the environment of
* the program the statement runs (`… TEST_MODE=fullstack <here>npx playwright test`). Null
* inside a nested `sh -c` string, for the same reason as `start`.
*/
assignmentOffset: number | null
/**
* The offset in the command one past the statement's last word — where an option of this
* statement's program belongs, before whatever closes it. Null inside a nested `sh -c` string.
*/
end: number | null
/**
* The offset in the command of the word in command position — the program, once every wrapper
* in front of it is taken away (`npx --prefix /app <here>playwright test`). Null inside a
* nested `sh -c` string, and for a statement that only sets variables.
*/
programOffset: number | null
/**
* The separator the shell split at immediately before this statement — `|`, `||`, `&&`, `;`,
* a newline or `&` — or '' when it is the first statement of the command. It is the parse's own
* answer, so it is known even where the offsets are not, and a caller deciding whether two
* statements are one pipeline (`|`, never `;`) asks it instead of reading the text between them.
*/
separator: string
/**
* The separator that ENDED this statement — `&` when the shell backgrounds it (so the process
* writes its own output, not the command's), '' for the last statement of a command.
*/
terminator: string
/**
* The file this statement sends its own output to (`> out.log`, `>> log`, `&> run.log`,
* `| tee file`), or null. Only a redirect written at the command's own level counts: one
* inside a `( … )` / `{ … }` group writes through that group, and one that feeds a heredoc's
* file (`cat > cfg.ts <<'EOF'`) is writing a config (X1 flash-m44, flash-m136).
*/
redirectTarget: string | null
}
SuiteSummary
What a runner's own closing summary reports, and whether it accounts for the whole run.
A summary is not by itself proof that the run finished. Playwright prints one when its process
is stopped too (SIGTERM/SIGINT), listing only the tests it reached — so a run killed at 36 of
139 tests ends with the same 36 passed (2.5m) line as a run that walked all 139. The only
thing that separates them is the total the runner announced before it started
(Running 139 tests using 1 worker), which suiteSummary reconciles the summary against.
interface SuiteSummary {
/**
* The total the runner announced before its first test, or null when it announced none (Hurl
* announces nothing; Vitest's own summary line carries its total in parentheses).
*/
announced: number | null
/**
* Every test the summary accounts for — the ones it passed, failed, marked flaky, timed out,
* interrupted, skipped or left "did not run". This is how far the walk got, which is what a
* stopped run's sentence says; a summary that accounts for fewer than `announced` was
* printed by a process that stopped early.
*/
accounted: number
/** Whether the summary accounts for every test the runner announced. */
finished: boolean
}
CutRun
A run a budget stopped, as its result reports it.
interface CutRun {
/** The bound the run was allowed, in seconds. */
seconds: number
/** Whether this tool's budget stopped it or the `timeout` the command carried. */
stoppedBy: CutBy
/**
* Whether this was a detached background task, read with no budget of this tool's own
* (`commandCutRun`'s `budgetSeconds` is 0). A background run is bounded by what its own
* command carries — so a `command-timeout` cut there is that prefix killing the walk, and the
* executor must be told to drop the prefix, never to start the run in the background it is
* already in (X1 xr-molecule-flash-m143 +24:45).
*/
background: boolean
/**
* The exit code the run's own process reported, or null when the code in the result belongs to
* the pipeline's filter (`… | tail`) rather than to the run — the case that read as success.
*/
exitCode: number | null
/**
* How far the run got, as its own output counts it — the files that finished, or the tests its
* summary accounts for — or null when the output says nothing.
*/
ran: number | null
/**
* How many the run was walking: the total its runner announced (`Running 139 tests`), or
* Playwright's `[n/K]`, whichever the output carries; null when neither does.
*/
total: number | null
/** The noun the runner counts in. */
noun: 'files' | 'tests'
/** The last thing the run printed — the line a reader looks at first. */
doing: string | null
}
RunFailureClass
One failure a runner printed, classified by the layer that failed.
interface RunFailureClass {
/** The layer that failed. */
kind: RunFailureKind
/** For a `connection` failure, what the transport did. */
connection?: ConnectionFailureKind
/** How long the request waited before the transport gave up, in seconds, when printed. */
seconds?: number
/** The `file:line` the runner printed for the failure, when it printed one. */
at?: string
}
NormalizableSchema
The JSON-Schema-ish shape every tool in this package declares.
interface NormalizableSchema {
name: string
parameters?: {
type?: string
properties?: Record<string, { type?: string; description?: string }>
required?: string[]
}
}
NormalizedInput
The result of normalizing one tool call's input.
interface NormalizedInput {
/** The input to hand the handler, with aliases moved onto the declared names. */
input: Record<string, unknown>
/** Declared-required parameters still absent after aliasing. */
missing: string[]
/** `alias→declared` pairs that were moved, for logging. */
aliased: string[]
}
GuardOptions
Options shared by guardToolExecute and guardAITool.
interface GuardOptions {
/**
* Redactor applied to a THROWN error's message before it is returned to the
* model. Defaults to this package's `redactSecrets`.
*/
redactError?: (message: string) => string
}
RunnerOutputFilter
A cutting stage that follows the runner, and the command with the whole pipe removed.
interface RunnerOutputFilter {
/** The stage exactly as the runner's command wrote it — `tail -6`. */
stage: string
/** The pipe as a sentence names it — `| tail -6`. */
pipe: string
/**
* The command with the pipe and the stage removed — `hurl --variable uid=… --test /tmp/t.hurl
* 2>&1`. This is a command to send again as written, not a re-typing of it: it is a slice of the
* original with the filter's own range cut out, so every other word, quote and redirect stays.
*/
command: string
}
ToolSchema
JSON-schema-backed definition for a single agent tool.
interface ToolSchema {
name: string
description: string
parameters: JSONSchema
}
LeadingSleep
The leading sleep a command opens with, and the command that follows it.
interface LeadingSleep {
/** How many seconds the statement would have slept (GNU sleep sums its operands). */
seconds: number
/** What follows the leading statement, trimmed — '' when the sleep was the whole command. */
rest: string
}
SuiteRunInFlight
One suite run, as the rule that decides whether a second may start beside it reads it.
Both callers describe the runs in flight the same way and ask the same question of them; what differs is only which runs each one knows about — the hold reads its ledger of background runs, the note reads the one run it is waiting on.
interface SuiteRunInFlight {
/** Whether the run invokes a suite at all: a `vitest`/`jest` run is not one, and cannot be. */
runsSuite: boolean
/** The app and database the run measures, as `suiteCommandTarget` keys it. */
target: string
/**
* Whether the command being judged STOPS this run before starting its own — a kill comes first,
* so a run the command itself stops is not in the way (xr-molecule-flash-m87's
* `kill 19408 … ; npx playwright test …`).
*/
stoppedByCommand?: boolean
}
InstalledPackageFacts
What the filesystem says about a write target under node_modules — the half
of the decision a path's spelling cannot make.
interface InstalledPackageFacts {
/** Whether the `node_modules` entry the path goes through is a symbolic link. */
readonly symlink: boolean
/** Whether a package is really installed there: a `package.json` resolves under the entry. */
readonly installed: boolean
}
TraceRequest
One request a traced page made, as *.network records it.
interface TraceRequest {
/** The HTTP method (`POST`). */
method: string
/** The request's path (`/api/articles/x/favorite`), or the whole URL when it is cross-origin. */
path: string
/**
* The response status as the trace recorded it. Playwright writes `-1` when a request was never
* answered (its HAR entry starts at `-1` and only a response event overwrites it); `0` is the
* same absence when the snapshot carried no response at all. Neither is a server failure.
*/
status: number
/**
* Playwright's own error text for a request that failed WITHOUT a response
* (`net::ERR_CONNECTION_REFUSED`) — a real failure. A request that got no response and recorded
* no text was aborted or still in flight when the test ended (the page closed under it), so it
* says nothing about the server (xr-molecule-flash-m112 +30:45).
*/
failure?: string
/**
* What the page SENT, for a 4xx answered by the app's own API — bounded (see
* `MAX_TRACED_BODY_CHARS`). A body wrapped as `{"data":{…}}` is visible here in one line:
* app-http's verbs take the body as their second argument, so a helper that passes
* `{ data: body }` there ships the wrapper (xr-molecule-flash-m111 +11:36).
*/
sent?: string
/** What came back for that 4xx, bounded the same way — the API's own error body. */
got?: string
/**
* The browser's own resource type (`document`, `fetch`, `xhr`, `font`, `image`, `stylesheet`,
* `script`, …). `document` when the request was the page load itself; when it ended in a
* `net::ERR_*`, nothing rendered and no selector or app request was measured.
*/
type?: string
/**
* When the request started, on the trace's own monotonic clock (ms) — the same clock the
* `TraceAction`s carry, so a request can be attributed to the action that caused it.
* Absent on traces whose network entries recorded no clock (an older Playwright, a synthetic
* fixture), in which case only recording order is available.
*/
startedAt?: number
/**
* Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
*
* A 401 cannot be read without this: the API's own body said
* `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
* when the header's absence proved the CLIENT never attached the token
* (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
* refused a token it was sent.
*/
sentAuthorization?: boolean
/**
* The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
* no header or its first word does not parse as a scheme (a value pasted without one is a
* credential, and is never copied here).
*/
authorization?: string
/**
* Whether the request carried a `Cookie` header — never the cookies themselves. An app that
* signs in with a session cookie has no `Authorization` header by design, so this tells a
* credential-less request from one whose credential is a cookie.
*/
sentCookie?: boolean
}
TraceLastAction
The test's LAST user action before the failing assertion, and the requests it caused.
An assertion after a navigation is not explained by the page's last call (GET /api/tags on
whatever page the app redirected to); it is explained by what the action the test took sent and
what came back (xr-molecule-flash-m148: 13 then 12 failing tests, every trace row leading with
GET /api/tags → 200, while the register POST /api/users and its answer — the fact that
explains the signed-out page — was never shown).
interface TraceLastAction {
/** The Playwright API the trace names: `Locator.click`, `Locator.fill`, `Frame.goto`. */
api: string
/** When the action started, on the trace's monotonic clock (ms). */
start: number
/** When it finished, or null when the run ended before it did. */
end: number | null
/**
* The requests the action caused — started after it and before the failing assertion, the app's
* own API calls first, static assets and the page shell's own files left out. Empty when the
* action sent none.
*/
requests: TraceRequest[]
/** The main frame's URL when the action ran, when the trace recorded frame snapshots. */
pageBefore?: string
/** The last main-frame URL the trace recorded — where the page ended up. */
pageAfter?: string
}
TraceAction
One action the traced page performed, with the trace's own timing for it.
interface TraceAction {
/** The Playwright API the entry names: `Frame.goto`, `Locator.fill`, `Locator.click`. */
api: string
/** When the action started, on the trace's own monotonic clock (ms). */
start: number
/** When it finished, or null when the run ended before it did. */
end: number | null
}
TraceArtifact
What one trace says: the test it belongs to, the requests it made, and its console lines.
interface TraceArtifact {
/** The failure's spec file, relative to the run's directory, when the trace names one. */
spec: string | null
/** The line of the test's last traced step, 0 when the trace names no spec. */
line: number
/** The requests the page made, the interesting ones first (`traceArtifactsScript`). */
requests: TraceRequest[]
/**
* The request the failing step itself sent: the first answered request the test's last user
* action caused (`TraceArtifact.lastAction`), else — on a trace with no action clock — the
* trace's LAST `fetch`/`xhr` in recording order, else its document. It leads the note's row when
* `lastAction` is absent, because a click that changed nothing is explained by the request
* the click sent and what came back — xr-molecule-flash-m135's Follow click sent
* `DELETE /api/profiles/x/follow → 200` (its `following` state was stale) and the note listed only
* the test's earlier, answered requests.
*
* Absent when the trace recorded neither, and on traces read before this field existed.
*/
lead?: TraceRequest
/**
* The console lines and page errors the page raised: its errors first, then a line that names a
* session decision or comes from a molecule package, then any other warning, then the browser's
* own resource error (`traceArtifactsScript`).
*/
console: string[]
/**
* The test's LAST user action before the failing assertion and the requests it caused — what the
* row leads with, when the trace carries the action times (`TraceLastAction`).
*
* Absent on traces read before this field existed, and on traces whose network entries recorded
* no clock; the row then falls back to `lead`.
*/
lastAction?: TraceLastAction
/**
* The page's own actions in the order the trace recorded them, bounded — what a test that ran
* out of a per-test budget spends its time on. A test that repeats one action prints it once per
* repeat, so the gap between two starts is one repeat's measured cost.
*/
actions: TraceAction[]
}
TraceRunFailure
What the runner's own report says about ONE failing test: where the test is declared, which Playwright call its failure names, and the per-test deadline the failure measured against.
A trace cannot tell a navigation that ran out of budget from a selector wait that did. On
xr-molecule-flash-m153 a 15 s page.waitForSelector timeout left a trace whose Frame.goto
never finished (the test was cut off mid-navigation) with a /src/components/*.tsx module still
pending when the context closed — and the note read that as an asset hang, then sent the run to
re-run the same tests unchanged against the same dev server while the same result's
testRunNote was telling it to measure a production build.
interface TraceRunFailure {
/** The spec file the runner's heading names (`e2e/url-navigation.spec.ts`). */
spec: string
/** The line the test is DECLARED on, from that heading (`206`). */
line: number
/** The Playwright call the failure names (`page.waitForSelector`, `page.goto`), or null. */
operation: string | null
/** The per-test deadline the failure names, in ms (`15000`), or null when it names none. */
budgetMs: number | null
}
TraceRunReport
The runner's own report, beside the traces it wrote.
interface TraceRunReport {
/**
* One entry per failing test, as the runner printed them. Empty when the caller supplied none:
* without the runner's reported operation a pending asset is never called the cause.
*/
failures?: readonly TraceRunFailure[]
/**
* True when the same result's `testRunNote` already names a production build as the next
* measurement (a local dev-server walk that ran out of its per-test budget). The note then
* states the ONE next step instead of directing an unchanged same-target re-run that
* contradicts it.
*/
productionBuildAdvised?: boolean
}
DiagnosisTraceRewrite
A command with the trace flag this platform decided to add, and the sentence to report it in.
interface DiagnosisTraceRewrite {
/** The command to run. */
command: string
/** One sentence naming the flag and why, or null when nothing was added. */
change: string | null
}
ExecutionBackend
Abstraction over the execution environment. Implemented by SandboxBackend (Docker) and LocalBackend (host filesystem).
interface ExecutionBackend {
/** The root directory for all operations (e.g. '/workspace' or '/Users/.../project'). */
readonly projectRoot: string
/**
* Marks backends that operate DIRECTLY on the host filesystem (the local
* backend), as opposed to a contained sandbox. Consumers use it to judge
* how loud to be about disabled safety guards — a `..` path or symlink
* escape on a host backend reaches the operator's real files, while the
* same escape in a sandbox stays contained.
*/
readonly hostFs?: boolean
/** Read a file's content as UTF-8 string. */
readFile(path: string): Promise<string>
/** Write content to a file. Creates parent directories as needed. */
writeFile(path: string, content: string): Promise<void>
/** Delete a file. */
deleteFile(path: string): Promise<void>
/** List entries in a directory. */
readDir(path: string): Promise<Array<{ name: string; type: 'file' | 'directory' }>>
/**
* Run a shell command. Returns stdout, stderr, and exit code.
* Backends implement this safely (sandbox.exec for Docker, execFile for local).
*/
run(
command: string,
opts?: {
cwd?: string
timeout?: number
/**
* A budget (ms) the backend enforces INSIDE its own shell, after any
* environment sourcing a consumer wraps around the command — so an
* overrun is stopped where it runs and the output so far still comes
* back, with exit code 124. A backend that cannot enforce it ignores it.
*/
budgetMs?: number
},
): Promise<{ stdout: string; stderr: string; exitCode: number }>
}
ToolBuildConfig
Configuration for building the tool set. Allows consumers to customize security, callbacks, and tool selection.
interface ToolBuildConfig {
/** Which tools to include. Defaults to all. */
include?: string[]
/** Which tools to exclude. Applied after include. */
exclude?: string[]
/** Whether to validate paths stay within projectRoot. Default: true. */
pathGuards?: boolean
/** Whether to check symlinks resolve within projectRoot. Default: false (sandbox-only). */
symlinkGuards?: boolean
/** Whether to redact secrets in file reads and command output. Default: true. */
redactSecrets?: boolean
/** Whether to block dangerous shell commands (env dumps, /proc access). Default: false. */
blockDangerousCommands?: boolean
/**
* Consumer-specific command guard for `exec_command`, checked BEFORE execution (after
* the built-in dangerous-command check). Return an error string to block the command —
* it is returned to the model verbatim, so make it actionable (say what to do instead) —
* or `null`/`undefined` to allow it. Keeps environment-specific rules (e.g. an IDE
* sandbox forbidding installs that would break its preinstalled library) out of this
* shared package.
*
* @param command - The shell command the model asked to run.
* @param cwd - The resolved working directory it would run in.
* @returns An error string to block, or null/undefined to allow.
*/
blockCommand?: (command: string, cwd: string) => string | null | undefined
/**
* Timeout (ms) for a single `exec_command` run. `exec_command` legitimately
* runs LONG — `npm install`, a production build, a test suite — so the default
* is generous (2 min); the old 30 s hardcap killed those spuriously. A consumer
* that wraps tool calls in its own outer timeout should set this to match (or
* slightly exceed) that budget so its own timeout is the effective bound and
* produces the nicer "tool timed out" message. Quick commands are unaffected —
* this is only the ceiling before a wedged command is killed.
*/
execTimeoutMs?: number
/**
* Budget (ms) for a single `exec_command`, passed to `backend.run` as
* `budgetMs`: the backend runs the command under `timeout` in its own shell,
* so when it overruns it is stopped there and the tool still returns
* everything it printed until then (exit code 124 plus an `error` naming
* the limit). Without this, an outer per-tool timeout races the run and
* discards minutes of build or test output along with the result. Set it a
* little under that outer timeout. Unset = no budget.
*/
commandBudgetMs?: number
/**
* How long (ms) exec_command waits for a command it backgrounded only because the
* command asked for more than `commandBudgetMs`, before handing back a task
* id; one that finishes inside it returns like a foreground run. Default 45000.
*/
overCeilingGraceMs?: number
/**
* Directory names `search_files` and `find_files` skip (VS Code
* `search.exclude` semantics). Defaults to `DEFAULT_SEARCH_EXCLUDED_DIRS`
* (node_modules, VCS dirs, build output). Pass the consumer's per-project
* setting so every search surface shares ONE synchronized set.
*/
searchExcludedDirs?: string[]
/**
* Whether a LARGE HTML document should come back as its OUTLINE — the head's
* tags and the tags that open each body section, one line each, plus a note
* naming the offset/limit that returns the whole document — instead of its
* whole text. Over `MAX_HTML_INLINE_CHARS`, and only when the read named
* no line window.
*
* A getter, not a boolean, because the answer belongs to the TURN, not to the
* tool set: a host that plans first and builds after wants an outline while the
* executor is researching (where a read it cannot act on is pure context cost)
* and the document itself while it is writing the page. Unset means every big
* HTML read returns its text, which is what a host with no such phases wants.
*
* @returns True when a large HTML read should be outlined rather than returned.
*/
outlineLargeHtml?: () => boolean
/**
* The test titles the person's request EXCLUDES by name (molecule-dev's `excludedTestTitles`),
* asked when a finished suite run's cause note is built. Their failures are not causes to fix:
* the note drops them, says so at the end, and inverts them out of the narrowed re-run it hands
* over — a result must never name a test beside "it does not count" (xr-molecule-flash-m101
* +12:00, where the executor spent a turn and an edit on the excluded test).
*
* A getter, not an array, because the request is read per turn while the tool set is built
* once. Unset means no test is excluded.
*
* @returns The quoted titles the request carves out.
*/
excludedTestTitles?: () => readonly string[]
/** Post-write hook (e.g. auto-format via Prettier/ESLint). Called after every write_file/edit_file. */
onAfterWrite?: (path: string) => Promise<void>
/** Diff tracking callback. Called before writes with old/new content. */
onFileDiff?: (event: FileDiffEvent) => void
/** Structural change callback. Called on create_directory, delete_file, rename_file. */
onFileChange?: (event: FileChangeEvent) => void
}
FileDiffEvent
Payload emitted when a tracked file changes contents.
interface FileDiffEvent {
path: string
oldContent: string | null
newContent: string
}
FileChangeEvent
Payload emitted when a file is created, modified, or deleted structurally.
interface FileChangeEvent {
type: 'created' | 'modified' | 'deleted'
path: string
}
SkillEntry
Metadata for a discovered skill (used in PromptContext).
interface SkillEntry {
/** Skill name. */
name: string
/** Short description. */
description: string
/** Relative path to the SKILL.md file. */
path: string
}
PromptContext
Context for building a composable system prompt.
interface PromptContext {
/** Agent identity (e.g. 'Synthase', 'Polish Agent'). */
agentName: string
/** Project root path. */
projectRoot: string
/** Names of available tools (for the tool listing section). */
tools: string[]
/** Project-specific rules (AGENTS.md or CLAUDE.md content). */
projectDocs?: string
/** Additional skill/reference content to inject. */
skills?: string[]
/** Discovered skills to list in the prompt (use load_skill to read on demand). */
discoveredSkills?: SkillEntry[]
/** Custom sections to append to the prompt. */
customSections?: string[]
}
SuiteFilterStrip
A command with the output filters after its suite runner removed.
interface SuiteFilterStrip {
/** The command to run. */
command: string
/** The pipeline stages dropped, as written (`| grep … | tail -20`), or '' when none were. */
dropped: string
}
SuiteRunPipelinePlacement
Whether a finished run's exit code belongs to a stage after the suite runner.
interface SuiteRunPipelinePlacement {
/**
* True when a pipe this tool must not drop still follows the suite runner: the code in the
* result is that stage's, not the run's.
*/
piped: boolean
/**
* True when the suite (or a stage of its pipeline) sits inside a nested `sh -c '<command>'`
* string, so the parse cannot place it in the command's own text. The caller then says so
* plainly instead of implying a stage it can point at — and keeps the command's own code when
* no pipe follows the runner, because on that command the pipeline's last stage IS the run.
*/
unplaced: boolean
}
FailedTest
One failed test, as a running suite's log names it.
interface FailedTest {
/** The location the log writes (`specs/e2e/articles.spec.ts:32`), or '' when it writes none. */
where: string
/** The test's own title — the last segment of its title path. */
title: string
/** How long it ran before failing, in seconds, or null when the log does not say. */
seconds: number | null
}
RepeatingFailure
A cause repeating among a running suite's failures.
interface RepeatingFailure {
/**
* How many DISTINCT tests share the signature — a test printed twice (two reporters in the
* log) or failed twice (a retry) is one test.
*/
count: number
/** The shared error line, when the log carries the failures' bodies (`line` reporter). */
message: string | null
/** The shared duration in seconds, when the failures line up by how long they ran. */
seconds: number | null
/** The distinct tests that share it, in log order. */
tests: FailedTest[]
}
UniversalCause
A repeating cause that is failing the WHOLE run — see universalCause.
interface UniversalCause {
/** How many distinct tests share it. */
count: number
/** The distinct spec files they live in, in the order the log named them. */
files: string[]
/**
* The cause is that the page rendered NOTHING: every failure so far is a wait for an element
* the page never painted, whatever the selector asked for (`emptyPageCause`). One cause
* is failing every test even though no two failures share an error signature, so the note says
* so in the page's terms instead of naming one locator as the signature.
*/
pageRendersNothing?: true
/** The page's own uncaught error, when the run printed one. */
pageError?: string
/**
* The distinct failures this cause covers, in log order — the rows a note lists for
* `pageRendersNothing`. Absent for a signature cause, whose rows are the repeating
* failure's own tests.
*/
tests?: FailedTest[]
}
FailureCause
One cause of failure in a run, and the tests it hit.
interface FailureCause {
/** The error line its tests all reported, or null when only the duration ties them together. */
message: string | null
/** The duration they shared in seconds, or null when the error line is the signature. */
seconds: number | null
/** The distinct tests it hit, in the order the log named them. */
tests: FailedTest[]
/**
* The FIRST failure's two most useful body lines — the values that differ, or the locator from
* its `Call log` (`failureEvidence`) — so a fix round needs no re-read of the log. Empty
* for a duration group, whose blocks carried no bodies.
*/
evidence: string[]
/**
* The suite's own per-test budget in ms, when the cause is the test-budget kill
* (`Test timeout of 15000ms exceeded`). The signature normalizes the number away
* (`normalizedSignature`) so equal causes group, and that number is the one fact that
* predicts a test spending its budget on a repeated action (xr-molecule-flash-m110 +20:44).
*/
budgetMs?: number
/**
* The layer the failure came from (`classifyRunFailure`). A `connection` cause never
* reached the app, so it is NOT a cause the app's code can fix: the note says to re-run it
* once and read the app's log on a repeat, instead of sending a fix at a handler
* (xr-molecule-flash-m110 +18:42 rewrote a CORS middleware for a 20 s connection timeout).
*/
kind: RunFailureKind
/**
* What the page itself showed at the moment of the failure, when the caller read it from
* Playwright's `error-context.md` (the alert / error list). The failure's own error line names
* the symptom — `waitForURL` timed out — and never what the page was saying: xr-molecule-flash-
* m122 +23:39 grouped eight `waitForURL` timeouts under one line while the page snapshots named
* the cause (`email can't be blank`, `username can't be blank` — the settings form submitted
* before its values had loaded). Attached to the cause so the fix needs no `cat` of the file.
*/
pageShowed?: string[]
}
CausePageErrors
One failing test's page errors, as read from its error-context.md.
interface CausePageErrors {
/** The test's location as the log prints it (`settings.spec.ts:107`). */
where: string
/** The alert / error items the page showed at the moment it failed. */
items: readonly string[]
}
Types
CommandRunState
What a command's result records: a run that ended, or one a budget stopped.
type CommandRunState = 'ran' | typeof CUT_AT_BUDGET
CutBy
What stopped a run.
type CutBy =
/** This tool's own budget (the backend's `timeout`, just past it). */
| 'tool-budget'
/** The `timeout <N>` the command itself carried. */
| 'command-timeout'
/**
* Something outside the run: the runner's summary does not account for the total it announced,
* and neither of the two bounds above had fired — the process was killed by something this
* platform does not own (the sandbox's own process supervisor, the box going away, a signal).
*/
| 'process-stop'
RunFailureKind
The layer a failure came from: the transport (no answer at all), the response's status, or an assertion over a response that arrived.
type RunFailureKind = 'connection' | 'status' | 'assertion'
ConnectionFailureKind
What the transport did when a request never got an answer.
type ConnectionFailureKind = 'timeout' | 'refused' | 'reset' | 'dns' | 'tls' | 'empty'
DiscoveredSkill (deprecated)
Re-export SkillEntry as DiscoveredSkill for backwards compatibility.
type DiscoveredSkill = SkillEntry
InstalledPackageProbe
Looks at the filesystem for InstalledPackageFacts.
The guard cannot stat the path itself: the tools act through a backend (a
sandbox over the network for the fleet), so the caller — which holds that
backend — supplies the look. shellInstalledPackageProbe builds one
from any shell runner.
type InstalledPackageProbe = (path: string) => Promise<InstalledPackageFacts>
Classes
ToolInputError
A tool call the handler refuses with a message meant for the model as is —
thrown from deep inside a handler (a path check), returned by the guard as the
call's error without the generic "check your parameters" wrapping.
class ToolInputError extends Error {}
Functions
createLocalBackend(projectRoot)
Create an ExecutionBackend that operates on the local filesystem.
function createLocalBackend(projectRoot: string): ExecutionBackend
projectRoot— Absolute path to the project root directory
Returns: A backend wired to fs/promises and guarded subprocess calls.
createSandboxBackend(sandbox, projectRoot)
Create an ExecutionBackend that delegates to a Docker sandbox instance. The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): ExecutionBackend
sandbox— A running Sandbox instance from@molecule/api-code-sandboxprojectRoot— Root directory inside the sandbox (default: '/workspace')
Returns: A backend that proxies all filesystem calls into the sandbox.
durationSeconds(word)
The seconds a duration word stands for.
function durationSeconds(word: string): number | null
word— A duration (280,20s,1m).
Returns: The seconds, or null when the word is not a duration.
commandSegments(command)
The segments of a command, each with its program and arguments — the answer every classifier that reads a command as "this runs the suite" is asking for.
function commandSegments(command: string): CommandSegment[]
command— The shell command.
Returns: One entry per statement or pipeline stage that runs something, in order.
locatedCommandSegments(command)
Every statement of a command, with its own assignments and where it sits — assignment-only statements included, because the variables those persist are exactly what a later statement of the same command reads.
function locatedCommandSegments(command: string): LocatedCommandSegment[]
command— The shell command.
Returns: One entry per statement or pipeline stage, in order.
statementArgumentEnd(command, start, end)
Where an option of a statement's program belongs: one past the statement's last ARGUMENT.
A flag written "at the end of the command" is not the runner's: a statement can end on a
redirection (… 2>&1), and a statement can sit inside a group whose closer belongs to the
shell, not to the words (( … ), { …; }). Appending after that closer is a different shell
word that the group has already closed — … --config=x 2>&1) --reporter=list is
sh: Syntax error: word unexpected, which killed a backgrounded walk at once and pushed the
model off the platform's runner shape (xr-molecule-flash-m129 +34:29). Write the option where
the program's own arguments end and the rest of the statement still parses:
… --config=x --reporter=list 2>&1).
The statement must be one locatedCommandSegments placed in THIS command: a statement
inside a nested sh -c '<command>' string has no offsets here (its positions are positions in
that string), so null is returned rather than a splice into the outer text.
function statementArgumentEnd(
command: string,
start: number | null,
end: number | null,
): number | null
command— The shell command.start— The statement's first character (LocatedCommandSegment.start).end— One past the statement's last character (LocatedCommandSegment.end).
Returns: The offset to write the option at, or null when the statement cannot be placed here.
commandTimeoutSeconds(command)
The bound a command puts on its own run: the largest timeout <N> any of its segments runs
under (timeout 280 hurl --test … → 280), or null when it carries none.
This is the bound man timeout enforces by KILLING the command — so a run that used it did not
finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
other command reader uses, rather than matched as text: timeout in an argument
(grep -n 'timeout 280' notes.md) bounds nothing, and -k 5 is the grace before the kill, not
the bound (WRAPPER_VALUE_FLAGS).
function commandTimeoutSeconds(command: string): number | null
command— The shell command.
Returns: The seconds the command allows itself, or null.
withoutTimeoutWrapper(command, which)
The command with the timeout <N> wrapper lifted off the statements which selects, and the
largest bound that was lifted.
The platform moves a run to the background precisely because its own bound cannot fit this
tool's ceiling, and a background task has no ceiling — its wait is bounded by wait_for_task
and the turn's deadline. The timeout N an executor wrote to fit the ceiling therefore keeps
killing the run at N in the background, where nothing needed it: X1 xr-molecule-flash-m143
(+24:45) wrote timeout 280 for a 31-test walk, the tool backgrounded it, and the prefix stopped
the walk with no tally — then the cut sentence told the executor to "run it in the background",
which is where it already was.
The wrapper is found by the same parse every other command reader uses (commandStart):
timeout in an argument bounds nothing, and the wrapper's own flags (-k/--kill-after,
-s/--signal) are stepped over, so timeout -k 5 280 … is 280.
function withoutTimeoutWrapper(
command: string,
which: (segment: CommandSegment) => boolean,
): { command: string; seconds: number | null }
command— The shell command.which— The statements to lift the wrapper from (a suite run, for the background path).
Returns: The rewritten command, and the largest lifted bound (null when nothing was lifted).
runsInCommandPosition(command, pattern)
Whether any segment of the command runs a program whose command-position text matches
pattern — the one question a runner-kind table asks.
The pattern is matched against program and its arguments as one line, so it must be
anchored at ^; it must not carry the g flag (a pattern shared between calls would then
carry its own match position from one segment to the next).
function runsInCommandPosition(command: string, pattern: RegExp): boolean
command— The shell command.pattern— An anchored pattern for the runner's invocation.
Returns: True when a segment in command position matches.
commandRunState(result)
The state a command's result records — THE reader every verdict about a run goes through.
A result that carries no state is a run that ended: the state is written where the run is
stopped (commandCutRun), so a result without it was never stopped by a budget this
platform set or can see. Results produced before this state existed read the same way, which is
what keeps a past turn's tally readable.
function commandRunState(result: unknown): CommandRunState
result— The exec_command / wait_for_task result.
Returns: The state the result records.
suiteSummary(text)
What the runner's closing summary accounts for, or null when it printed none.
Three runners' summaries are read, each by its own wording: Playwright's per-status lines
(reconciled against its Running N tests announcement), Vitest's single Tests … line (its own
parenthesised total), and Hurl's Executed files: block (which it prints only at the end, and
which announces no total — so it is a finish by construction).
function suiteSummary(text: string): SuiteSummary | null
text— The run's stdout and stderr, RAW (a truncated copy can lose the announcement).
Returns: The summary, or null when the text carries none.
suiteTally(text)
Whether text carries a runner's closing summary.
A summary is NOT a finish on its own — a stopped Playwright run prints one too. Use
suiteSummary and its finished for that.
function suiteTally(text: string): boolean
text— The run's stdout and stderr.
Returns: True when the runner's own end-of-run summary is there.
suiteRunExitCode(exitCode, output, piped)
The exit code a finished suite run may report.
The code in a background result is the code of the command's LAST pipeline stage. When a stage
after the suite runner still consumes its output (| tee run.log, or a filter the platform could
not prove output-only), that stage produced the code, not the run — and … | grep … | tail -20
exited 0 for a run with a failure in it (xr-molecule-flash-m101 +25:54). No code is reported
then: the log's own tally decides.
The same reading covers a code of 0 beside a tally that names a failure: whatever ended the pipeline, a run whose log counts a failure did not pass.
function suiteRunExitCode(exitCode: number | null, output: string, piped: boolean): number | null
exitCode— The code the pipeline reported (null when it has none).output— The run's full log.piped— Whether a pipeline stage after the suite runner still consumes its output.
Returns: The code the run may report, or null when the code is the filter's or contradicts the tally.
commandCutRun(command, result, elapsedMs, budgetSeconds, rawOutput)
The run a command's result records as stopped by a budget, or null when the result is a run that ended (or says nothing about a run).
Evidence, in order — never a guess:
- the runner's own summary, when it printed one: it is a finish only if it accounts for the total
the runner announced. A summary that accounts for fewer is a walk a process stopped early, and
falls through to the branches below with its own counts (
suiteSummary). timeout's own 124 (the code the backend's wrapper returns whenever the TOOL's budget fired, whatever the pipeline ended with) — and, when the clock says the command's own smaller bound ran out first and less than the budget, that bound instead.- the command carried its own
timeout <N>, N is inside this tool's budget (a larger one never fires — the tool's does), the run used N, and no runner printed a closing summary: the run was killed at its own bound. Its exit code, when the pipeline handed one back, is the filter's. - the run used this tool's whole budget behind a filter that withheld its output, and printed no closing summary: the budget stopped it, and no code in the result is the run's.
- a summary that does not account for its announced total, with neither bound fired: something
outside this platform stopped the process (
CutByprocess-stop).
function commandCutRun(
command: string,
result: unknown,
elapsedMs: number,
budgetSeconds?: number,
rawOutput?: string,
): CutRun | null
command— The command the executor sent.result— Its result.elapsedMs— How long the command ran, in ms.budgetSeconds— This tool's foreground budget, in seconds (0 when it has none).rawOutput— The run's output BEFORE truncation, when the caller has it: the announcement a summary is reconciled against is the first line a long run prints, which is exactly what a size cap drops. Detection reads this; the sentence's last line still comes fromresult.
Returns: The stopped run, or null.
cutRunFacts(result)
The facts a result records about the run a budget stopped it in — what a reader needs to say how far the run got WITHOUT re-reading its log or re-deriving the counts.
The numbers are read from the runner's own output where it printed them (Hurl's per-file lines,
Playwright's [n/K]), so a caller that can also see the suite itself — the file count of the
*.hurl directory the run walked — fills in the total this could not know, and both then say
the same sentence (cutRunSentence).
function cutRunFacts(result: unknown): CutRun | null
result— The exec_command / wait_for_task result.
Returns: The facts the result carries, or null when it records no stopped run.
cutRunSentence(run)
The first line of a stopped run's result: how far it got, what stopped it, the last thing it printed, and what the reader must do next.
Written to be quotable by the model in one sentence and to leave no room for "nearly passing":
the counts come from the runner's own output (runProgress), and a total the caller
read elsewhere (the suite's own file count) fills in the "of N" it can then state. The sentence
says how many tests the stop left unrun — a number a reader cannot round up to "the suite
passed" — and hands over the one move that survives a long run: run it in the background.
function cutRunSentence(run: CutRun): string
run— The stopped run.
Returns: The sentence.
classifyRunFailure(text)
Classify ONE failure's text — a Hurl error: block, or a Playwright failure message/body.
The transport is read first: a block that carries a connection error AND an assertion is a request whose assertion never ran, so it is a connection failure. Only a block that carries no failure signature at all answers null — unknown is never a connection failure, because a misread transport hides a real app failure behind "just re-run it".
function classifyRunFailure(text: string): RunFailureClass | null
text— One failure's text, as the runner printed it.
Returns: The classification, or null when the text carries no failure this reads.
allConnectionFailures(failures)
Whether every one of the classified failures is a connection failure — the only shape in which no response the app produced was measured.
function allConnectionFailures(failures: readonly (RunFailureClass | null)[]): boolean
failures— The run's classifications, in printed order.
Returns: True when there is at least one and none of them is the app's.
normalizeToolInput(schema, raw)
Move known aliases onto the declared parameter names and report what is still missing. Unknown keys are left in place — handlers ignore them, and dropping them would hide a model's intent from anything reading the call log.
function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput
schema— The tool's declared schema.raw— Whatever the model sent as the tool input.
Returns: The normalized input plus the missing-required and aliased lists.
missingParamError(schema, input, missing)
The error a tool returns when a required parameter is still absent after aliasing. Model-facing, so it names the right parameter, what was actually sent, and the shape of a correct call — everything needed to fix it in one turn instead of concluding the tool is broken.
function missingParamError(
schema: NormalizableSchema,
input: Record<string, unknown>,
missing: string[],
): string
schema— The tool's declared schema.input— The normalized input (used to list the keys that were sent).missing— The required parameters still absent.
Returns: A single-sentence-per-clause error string.
guardToolExecute(schema, impl)
Wrap a tool handler so every call is normalized first and a handler that THROWS returns something the model can act on.
The guard deliberately does NOT pre-empt the handler when a required
parameter looks absent: several handlers accept a documented older shape
(edit_file's top-level old_string/new_string) or answer a missing
parameter with far better guidance than a generic message could (edit_file
telling the model to re-read the file). It steps in only where the handler
would otherwise crash — and when it does, a still-missing required parameter
is the accurate diagnosis for that crash, so it is reported as one.
function guardToolExecute(
schema: NormalizableSchema,
impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>,
options?: GuardOptions,
): (input: unknown, ...rest: never[]) => Promise<unknown>
schema— The tool's declared schema.impl— The raw handler.
Returns: A handler with the same signature, guarded.
guardAITool(tool)
Apply guardToolExecute to a ready-made tool object (the
@molecule/api-ai AITool shape: { name, description, parameters, execute }), returning a copy whose execute is guarded.
Use it at the point a consumer hands its full tool list to the model, so tools built outside this package get the same treatment as the ones built inside it.
function guardAITool(tool: T, options?: GuardOptions): T
tool— The tool to guard.
Returns: A copy with a guarded execute.
diffLineCount(oldLines, newLines)
Count truly added/removed lines between two line arrays using LCS.
Bounded by construction, so a whole-file edit cannot allocate a quadratic
table here: the longest common prefix and suffix are trimmed first (exact —
those lines belong to every optimal diff, and on the usual small edit they
shrink the table to the edited span), and what is left is capped (see
lcsLength).
function diffLineCount(
oldLines: readonly string[],
newLines: readonly string[],
): { added: number; removed: number }
oldLines— The original lines array.newLines— The modified lines array.
Returns: An object with the number of added and removed lines.
commandRunsNoTests(command)
Whether a shell command invokes a test runner and EVERY such invocation only prints — so the command runs no test at all.
A runner counts only where a shell would RUN it (commandSegments): the word in command
position of a segment, after its wrapper prefixes. A runner named in an argument is not an
invocation — head -30 run-api-tests-hurl.sh and grep -n playwright e2e/README.md name a
file to read, and a --list written inside a file the command writes is not a flag anyone runs
with (flash-m81).
A command that invokes no runner is NOT one of these (false): "the command runs no test" is
then true for ls, which is not the question any caller is asking. The question is whether
the runner invocation each caller read as a run is one.
A command that also runs tests is false however it is spelled: npx playwright install && npx playwright test installs and runs, so a caller that sees playwright test in it is
reading a real run.
function commandRunsNoTests(command: string): boolean
command— The shell command as the executor sent it.
Returns: True when the command invokes a runner only to print.
outputFilterAfterRunner(command, runs)
The output filter that follows the runner in command, or null when there is none.
Only a stage of the runner's OWN pipeline counts (its separator is |), and only when it is the
LAST stage of that pipeline: a | grep … | wc -l chain would leave a dangling pipe if the
middle stage were cut out, so a chain is not answered here at all. A filter before the runner
(cat run.log | tail -20 && hurl …) reads something else's output and is not this.
function outputFilterAfterRunner(
command: string,
runs: (segment: CommandSegment) => boolean,
): RunnerOutputFilter | null
command— The command as the executor sent it.runs— Whether a segment is the runner this run is about (the caller knows its own kind:CommandSegment.programis the program, once wrapper words liketimeout 280andnpxare taken away).
Returns: The filter and the unfiltered command, or null.
pipelineExitCodeNote(command, exitCode, output)
The sentence an exec_command result carries beside exitCode when the code belongs to a
pipeline's last stage rather than to the check that printed the output.
Two shapes, both from a hand-run check with an unguarded pipe:
- the code says 0 and the output carries failures (
tsc | headreportshead's 0); - the code is non-zero and the checker printed no failures (
tsc | head && tail missing.logreportstail's 1, and the checker was clean).
A command with no pipe, a pipe guarded by pipefail, a pipeline where no checker runs, or a
code and an output that agree all return null.
function pipelineExitCodeNote(command: string, exitCode: number, output: string): string | null
command— The command as the executor sent it.exitCode— The code the shell reported (the pipeline's last stage's).output— The command's raw stdout and stderr, for the checker's own text.
Returns: One sentence naming the stage the code came from, or null.
leadingSleepPeek(command)
The leading sleep of a command, when the first statement is one and it asks for at least
MIN_PEEK_SLEEP_SECONDS.
The statement is read by locatedCommandSegments — the same parse every other command
classifier uses — so only a sleep in command position of the FIRST statement counts. The end
of that statement is then found by walking forward from its own start: the statement is
sleep <duration>, whose words carry no quotes and open no substitution, so the first unquoted
;, newline or & ends it. A | is refused: dropping the left side of a pipeline would hand
the right side its stdin instead, and a || fallback would run on a success that never did.
function leadingSleepPeek(command: string): LeadingSleep | null
command— The shell command the executor sent.
Returns: The seconds and the remaining command, or null when the command does not open with a long sleep.
withoutHeredocs(command)
A command with its heredoc bodies removed: the runs, kills and hosts it names in a file it WRITES are not what the command itself does.
The header is kept (cat > run.sh <<'EOF' …EOF leaves cat > run.sh), which is what the
readers here want: everything before the body is still readable as the command's own words.
function withoutHeredocs(command: string): string
command— The shell command.
Returns: The command without its heredoc bodies.
suiteCommandTarget(command)
The server a suite command points at, as a comparable key: the host it names (HOST=,
BASE_URL=, PLAYWRIGHT_BASE_URL=, --variable host=, or a bare URL), or local for a run
that names none — a suite run in the sandbox goes to the sandbox's own app unless the command
says otherwise. Every localhost spelling collapses to local (one sandbox, one app, one
database).
function suiteCommandTarget(command: string): string
command— The suite command.
Returns: The target key.
namesSuiteConfig(command)
Whether a Playwright statement names its own config — the reading withSuiteConfig and
scopedRerunCommand share, so neither adds a second --config to a command that has one.
function namesSuiteConfig(command: string): boolean
command— A command, or the Playwright statement inside it.
Returns: True when the statement loads a config of its own.
withSuiteConfig(command, config)
The command with a --config added to the Playwright invocation it starts — or unchanged when
it already names one, or starts no Playwright run.
The flag rides the same statement the runner does: a cd, an env prefix and a timeout before
it are left exactly as they were, and so is everything chained after the run (| tail -50).
xr-molecule-flash-m113 (+19:09): the model's fix-round re-run dropped the
--config playwright.conduit.config.ts both earlier runs had carried, so Playwright started with
NO base URL and all 10 failures were page.goto: Cannot navigate to invalid URL — a failure list
that says nothing about the app. The platform composes commands back to the executor too (the
narrowed re-run of a finished run's failures), and a command it hands back has to carry the
config the run was measured under, never the shape that caused that finding.
function withSuiteConfig(command: string, config: string | null | undefined): string
command— The command as the executor sent it.config— The config the run loads, when the caller knows it.
Returns: The command with the flag, or the command unchanged.
scopedRerunCommand(command, locations, excluded, config, cwd)
The whole-suite command made scoped to the tests it should re-run instead: the same
invocation — same directory, same settings — with the named file:lines in place of the whole
suite, --last-failed dropped (it re-runs the tests that never ran too, X1 flash-m60) and
--retries=0 set (a re-run of a known failure does not need to pay for retries).
config is the config the run was measured under (withSuiteConfig): the narrowed re-run
carries it whenever the command itself does not, so a run that dropped --config cannot be
handed back a command with the same defect (xr-molecule-flash-m113: every failing test was
Cannot navigate to invalid URL because the re-run loaded no base URL).
function scopedRerunCommand(
command: string,
locations: readonly string[],
excluded?: readonly string[],
config?: string | null,
cwd?: string | null,
): string
command— The whole-suite run's command.locations— The failingfile:lines to re-run, as the run's own log printed them. A location the reporter printed relative to the runner's own directory (../../fixtures/…, which no absolute path contains) is resolved here (playwrightLocations), so the command this returns is one Playwright can match.excluded— The test titles the request carves out. They are inverted out (--grep-invert) so a re-run of the failing locations cannot report one of them again — xr-molecule-flash-m101 re-ran the whole 139-test suite and spent ~4 minutes to be told about a test the request had already said does not count.config— The config the run loaded, when the caller knows it; added when the command does not name one.cwd— The directoryexec_commandwas given, when it was; the base a location is resolved against when the command does notcdanywhere.
Returns: The scoped command, or the command unchanged when it runs no Playwright suite.
suiteRunInTheWay(candidate, inFlight)
The run in flight that makes a second run a hazard, or null when nothing is in the way.
This is the ONE rule the mid-run note and the suite-overlap hold both answer with: a suite run may start only when this returns null, because two suites against one app and database share its rows — the failures one causes the other cannot be told from real ones. A run that invokes no suite is never in the way, and neither is one the candidate command stops first.
function suiteRunInTheWay(candidate: { target: string }, inFlight: readonly T[]): T | null
candidate— The run about to start, by the app and database it would measure (suiteCommandTarget).inFlight— The suite runs already going, in the order the caller found them.
Returns: The first run in the way, or null.
isDiagnosisRun(command)
Whether a command walks a PART of a suite instead of all of it: a cap that stops it at a failure
count, the failures the last run printed (--last-failed, --only-changed), a title filter, or
named files/lines.
THE one reading of that question, in one place. Two sites act on the answer and must never disagree:
- the command rewriter that adds
--retries=0to a background Playwright run (./utilities.jsstreamingBackgroundCommand) — a diagnosis walks each failure once, while the run the suite is judged by keeps the suite's own retries; and - the platform's own notes (molecule-dev's
suite-cli-override.ts), which must not tell the executor to drop the--retries=0the platform itself has just added — the contradiction X1 flash-m74 and m75 filed, and m76 filed a third time.
Until xr-molecule-flash-m89 they were a regex and a predicate that only LOOKED alike, and the
gap was exactly the run the platform had asked for: its testRunNote says "run the whole suite
once with --max-failures=0", and the rewriter read that cap flag as a capped diagnosis and
stripped the suite's retries from the uncapped proof run that follows a deploy — the one run the
graded run is compared against (+25:03, +31:39, +36:40 and +49:57, the last two against the
deployed URL, where a network blip is likeliest and the retries it dropped are the ones that
absorb it).
--max-failures=0 is Playwright's own "no cap" (boundsFailures), so it is not a
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/suite-overlap.d.ts.)
function isDiagnosisRun(command: string): boolean
command— The command as the executor sent it.
Returns: True when the run is a diagnosis or a re-run, not the whole suite.
discoverSkills(backend)
Discover skills from a project directory.
Scans .agents/skills/ and .claude/skills/ for SKILL.md files.
Reads the YAML frontmatter of each to extract name: and description: fields.
function discoverSkills(backend: ExecutionBackend): Promise<SkillEntry[]>
backend— Execution backend to use for filesystem access
Returns: Array of discovered skills with name, description, and path
buildAgentPrompt(ctx)
Build a coding-focused system prompt from composable sections.
Returns a string that includes:
- Agent identity
- Available tools listing
- Coding best practices
- Tool argument formatting guidance
- Project docs (if provided)
- Discovered skills listing (if provided)
- Inline skills (if provided)
- Custom sections (if provided)
function buildAgentPrompt(ctx: PromptContext): string
ctx— Prompt construction inputs (tools, docs, skills, etc.).
Returns: Fully assembled system prompt text for the coding agent.
taskStopCommand(backend, taskId)
The command that stops a running background task, read from the sandbox's own pid files.
THE one builder for it, so every note that hands the executor a way to stop a run hands over the
same pid-only form: a pkill -f <something> matches the shell running it (its argv IS the
command), and molecule-dev/api/src/ai/pattern-kill-guard.ts holds exactly that — X1
xr-molecule-flash-m87's stuck note suggested kill $(cat /tmp/<id>.pid) 2>/dev/null; pkill -f <id>.sh, which the platform's own guard then refused, and the same shape cost a refused call at
+21:57. The pid tree is the whole of what the pattern was for: the detached shell and every
process under it.
function taskStopCommand(backend: ExecutionBackend, taskId: string): Promise<string>
backend— The execution backend.taskId— The task'smol-bg-…handle.
Returns: The command to hand the executor — kill <pids> — falling back to the task's own pid file when the walk comes back empty (a sandbox without pgrep, or a pid file already gone, in which case the pid-file form is still the one the platform's guard names).
moleculeDistReadNote(path)
The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
redirect below fires only when the read FAILS, so a model that knows the hoisted
layout reads node_modules/@molecule/<pkg>/dist/*.js happily and never learns the
documented API is one call away (X0 flash-m70: 24 such reads in 57 s, no note on
any of them). Carried as distNote rather than merged into note so a host that
has read_molecule_doc (molecule-dev's cadence note, which also names the functions)
can supersede it without string-matching.
function moleculeDistReadNote(path: string): string | null
path— The resolved path that was read.
Returns: The note, or null when the path is not inside a package's dist.
shellInstalledPackageProbe(run)
Build an InstalledPackageProbe from a shell runner (a backend's
run, or a sandbox's exec): one command reports whether the entry is a
link and whether a package.json resolves under it.
function shellInstalledPackageProbe(
run: (command: string) => Promise<{ stdout?: string } | null | undefined>,
): InstalledPackageProbe
run— Runs a command where the path lives.
Returns: The probe.
installedPackageWriteRefusal(path, probe)
The refusal for a write whose target is inside an INSTALLED package, or null
when the path is not under node_modules or nothing is installed there.
node_modules is not the project's code: nothing written there survives the
next install, no deploy carries it, and a sandbox whose dependency tree is
shared (the pre-installed superset) breaks every other run through it. X0
xr-molecule-flash-m91 (+04:43) called write_file on
node_modules/@molecule/api-resource-comment/dist/numbers.d.ts while reading
the package's declarations and clobbered its runtime module a call later
(module.exports = {} over the ESM handler its own code imports) — the write
answered ok: true, and 18 s went to noticing, npm pack-ing the package and
putting the file back. Reading these files stays allowed (that is how the
declarations above are read at all).
What is protected is the INSTALL, not the spelling: the decision comes from
InstalledPackageFacts, never from the path alone (m106 above). Three
things are the run's own to remove or replace —
- a
node_modulesthat is a SYMLINK:rm/unlinkremoves the link, never the tree it points at, so a link the run made cannot clobber anything; - anything under the run's own
.mol-scratch/directory; - a
node_moduleswith nopackage.jsonunder it (nothing was ever installed).
A path INSIDE the link is still decided by what resolves there: a link into a real tree is refused, because removing or writing through it reaches the
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/tools.d.ts.)
function installedPackageWriteRefusal(
path: string,
probe?: InstalledPackageProbe,
): Promise<string | null>
path— An absolute path a tool is about to write.probe— Looks at the filesystem where the path lives.
Returns: The one-line refusal naming the package, or null when nothing is installed there.
buildTools(backend, config)
Build a complete set of AI agent tools bound to an execution backend.
function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool[]
backend— The execution environment (sandbox or local filesystem)config— Optional configuration for security, callbacks, and tool selection
Returns: Array of AITool objects ready to pass to an AI provider
traceArtifactsScript()
The Node program the sandbox runs to read a failed run's traces: node <script> <dir> prints one
TRACE <json> line per trace.zip under <dir> (newest first, MAX_TRACE_ARTIFACTS of
them), each with the test it belongs to, the requests its page made and its console lines.
It is a standalone program, not an import, for the reason @molecule/api-code-sandbox runs it:
Playwright's own zip reader is not reachable from a project's node_modules, and a dependency
on unzip would make the read fail on every sandbox that has none. The zip is read with the
central directory and zlib.inflateRawSync — about forty lines, and the only thing that has to
be true of a trace.zip is that it IS one.
A request is kept over a static asset when it failed — a status >= 400, or no response at all
together with Playwright's own error text (-1 plus _failureText, a refused connection) — and
over anything else when it is the page talking to the app (fetch/xhr/document): a failing
test's trace holds the whole page load. A request with no response and NO error text was aborted
or still in flight when the test ended; it is kept too, but as what it is, not as a failure.
A request answered 4xx by the app's OWN API also keeps what the page SENT and the first of what
came back: Playwright stores either inline (text) or in a resource entry (_file), and both
are read. That is where a wrapped body shows: xr-molecule-flash-m111's helper passed
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/trace-artifacts.d.ts.)
function traceArtifactsScript(): string
Returns: The program's source, to write to a file and run.
parseTraceArtifacts(stdout)
The traces traceArtifactsScript printed, parsed.
A line that is not a complete TRACE <json> (a truncated read, a program that died mid-write) is
dropped: a partial trace is not evidence about the test it came from.
function parseTraceArtifacts(stdout: string): TraceArtifact[]
stdout— The program's output.
Returns: One entry per trace it printed.
isStaticAssetRequest(request)
Whether a request is one of the page shell's, not the test's (STATIC_ASSET_PATH).
The document itself is never incidental — a page that never loaded IS the failure — and a
request whose type is the app's own API (fetch/xhr) is judged by its path alone.
function isStaticAssetRequest(request: TraceRequest): boolean
request— One request a trace recorded.
Returns: True when the request is incidental to the step under test.
failedRequest(request)
Whether a trace recorded a request the server actually failed: a 4xx/5xx, or a network error.
A 404 on a static asset is excepted: /sw.js and /fonts/* 404 on an app that never shipped
them, and naming one as a failure sent m125 into writing the caching worker it had asked about.
A 5xx on an asset, or a network error, still counts — the server did fail.
function failedRequest(request: TraceRequest): boolean
request— One request a trace recorded.
Returns: True when the request failed in a way the note should name.
unansweredRequest(request)
Whether a trace recorded NO response and no reason for it — a request that was aborted or still in flight when the test ended. It is not evidence that the server failed (m112 +30:45), and one the page shell made is not evidence about the step at all (m125 +29:33).
function unansweredRequest(request: TraceRequest): boolean
request— One request a trace recorded.
Returns: True when the request is still in flight and worth naming.
stalledDocumentSubresource(request)
Whether a trace recorded a DOCUMENT SUBRESOURCE that never finished: a stylesheet, script or other asset the page itself loads, with no response and no error text — still in flight when the navigation timed out.
The page shell's own incidental requests (/sw.js, the manifest, a favicon, a font) are
excepted for the reason isStaticAssetRequest documents: m125's run was sent to write a
caching service worker by an aborted /sw.js. A pending /assets/*.css is different — the
navigation blocks on it, so it is the reason page.goto ran out of time rather than one of the
app's API calls (xr-molecule-flash-m135 +34:10: three deployed page.goto timeouts on tests that
pass locally, one 88 KB stylesheet fetch measured at 27 s, and the failing tests' traces each
held the never-answered CSS while the note said "the cause is in the page").
function stalledDocumentSubresource(request: TraceRequest): boolean
request— One request a trace recorded.
Returns: True when the request is a document subresource that never finished.
failureTraceNote(items, target, earlierFailures, apiPrefixes, report)
The note a failed suite run's result carries: what each failing test's page actually sent, and what came back.
This is the answer to the question a swallowed error hides — a click that changed nothing is
usually a request that failed — so the note leads with the failing requests and says what to do
with them: read the handler the route reaches, not the selector. It is BOUNDED to
MAX_TRACE_NOTE_TESTS tests with MAX_TRACE_NOTE_REQUESTS requests each, because it
rides in a result the model pays for.
The three kinds are kept apart, because they mean different things. A FAILED request (4xx/5xx, or
a network error with Playwright's own text) is a cause. A request with NO RESPONSE RECORDED —
Playwright's -1, no error text — was still in flight when the test ended (the page closed under
it); listing it as a failure sent a run hunting an API that never answered anything wrong
(xr-molecule-flash-m112 +30:45). The rest are the page's last answered requests.
A 4xx request carries what the page SENT beside its status, so a wrapped body is visible in one
line rather than inferred from the status; when one of those bodies is an object whose only key
is data, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
Every row opens with the test's LAST user action before the failing assertion and the requests
that action caused (TraceArtifact.lastAction) — started after it, before the assertion,
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/trace-artifacts.d.ts.)
function failureTraceNote(
items: readonly TraceArtifact[],
target: string,
earlierFailures?: readonly string[],
apiPrefixes?: readonly string[],
report?: TraceRunReport,
): string | null
items— The tracesparseTraceArtifactsread.target— Where the run went — the deployed host, as the note names it.earlierFailures— The failingfile:lines earlier walks of this suite left, so a repeat is ranked above a one-off. Empty when the caller knows of no earlier walk.apiPrefixes— The API path prefixes the app's client config names (['/api']), read from the app by the caller. Empty when the caller could not read one; the traces' own requests may still supply it. Never a hard-coded literal.report— The runner's own report for this run (TraceRunReport): each failing test's declared location, the operation its failure names and the deadline it names, plus whether the same result'stestRunNotealready points at a production build. A pending asset is called the cause only when the runner says the failing operation for THAT test is the navigation it was loading; without the report nothing is called a navigation stall.
Returns: The note, or null when no trace held a recorded response, a real failure or a console error.
suiteRecordsTraces(configText)
Whether a Playwright config records traces for the tests that fail.
Only on and retain-on-failure keep a trace.zip for a failure whatever the retries do: the
two retry modes need a retry to happen at all, and a diagnosis run is given --retries=0
(streamingBackgroundCommand), so on X1 xr-molecule-flash-m100 the run's own
trace: 'on-first-retry' recorded nothing — which is what left the failing tests' network log
unreadable. A config that sets no trace: at all records nothing either (Playwright's default
is off); so does one that cannot be read.
function suiteRecordsTraces(configText: string): boolean
configText— The config's source, or '' when none could be read.
Returns: True when the config records traces for failures.
diagnosisTraceCommand(args)
The command a DIAGNOSIS run needs to leave a readable trace behind, when the suite's config would record none.
The failing tests' requests are only readable if the run wrote them down (see the module doc),
and the config is where the decision about that lives — the platform does not edit the suite's
config, so it passes the flag instead, and only where it can do no harm: on a run narrowed to a
part of the suite or capped (isDiagnosisRun), never on the whole-suite proof run, and
never over a --trace the executor wrote itself. A capped walk is exactly the run whose
failures someone is about to work on; the proof run is graded as it is.
function diagnosisTraceCommand(args: {
command: string
cwd: string
readFile: (path: string) => Promise<string>
}): Promise<DiagnosisTraceRewrite>
args— The command, the directory it starts in, and the backend's file read.
Returns: The command to run and the sentence to report, or the command unchanged.
shellQuote(s)
Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes), single-quoted strings prevent command substitution ($(), backticks) and variable expansion.
function shellQuote(s: string): string
s— Raw string to wrap for POSIX shell single-quoted context.
Returns: A single-quoted shell literal representing s.
stripControlChars(s)
Strip C0 control chars (except tab, newline, CR) that break PostgreSQL JSONB and can cause rendering issues.
function stripControlChars(s: string): string
s— Arbitrary text that may contain disallowed control characters.
Returns: A copy of s with unsafe control characters removed.
redactSecrets(s)
Redact values of common secret/credential patterns in text output.
ENV-DUMP GRADE — includes the JSON KEY: 'value' passes, which key off the
NAME beside the value and therefore cannot tell a credential from ordinary
code that happens to use a keyword-ish identifier. Use this for .env reads
and command output (where an env dump is the actual threat); use
redactSecretsInCode for source-file content.
function redactSecrets(s: string): string
s— Log or command output that may contain.env-style secrets.
Returns: A redacted copy safe to surface to end users or models.
redactSecretsInCode(s)
CODE-SAFE redaction — the env-assignment pass of redactSecrets WITHOUT
the JSON KEY: 'value' passes.
Those passes match on the NAME next to a quoted value, so over source code they
replace legitimate content at enormous scale: forgotPasswordEndpoint: '/users/forgot-password', apiKeys: 'API keys', and every localized "Show
password" string all became '[REDACTED]'. Because the agent writes back the
content it reads, that token then lands in the user's project — measured at
10,952 of 27,919 flagship template files before this split.
No value heuristic can fix that: a legitimate password = 'TestPass123!' in a
test helper is indistinguishable from a real credential by shape. So the
name-keyed passes simply do not run over code. Credentials in source are still
caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
which is what actually catches a secret sitting under an innocuous name.
function redactSecretsInCode(s: string): string
s— Source-file content or other code-shaped text.
Returns: A redacted copy that preserves ordinary code verbatim.
printsOnlySource(command)
Whether a shell command only PRINTS source files (cat a.tsx, sed -n '1,40p' x.ts,
head -50 b.hurl): its output is file content and gets the code-safe redaction, like
read_file. Through the env-dump grade a printed type AuthState = 'authenticated' | …
came back as '[REDACTED]', and the executor wrote the mask into a new file (X1
flash-m58). Any segment that is not such a print, or any env file named, keeps the full grade.
function printsOnlySource(command: string): boolean
command— The shell command.
Returns: True when every segment prints source files only.
isEnvFilePath(path)
Whether a path is an env file, for which redactSecrets' full env-dump
treatment is appropriate rather than redactSecretsInCode. Matches
.env, .env.<suffix>, and <name>.env.
function isEnvFilePath(path: string): boolean
path— A workspace-relative or absolute file path.
Returns: true when the file is an env file.
declaredBudgetSeconds(command, timeoutParam)
The time budget a COMMAND declares for itself — a timeout <seconds> prefix
anywhere in it, or the tool call's own timeout (ms) parameter, whichever is
larger. Zero when it declares none.
function declaredBudgetSeconds(command: string, timeoutParam: unknown): number
command— The shell command.timeoutParam— The tool call'stimeoutinput, in milliseconds.
Returns: The declared budget in seconds, or 0.
outputIsWithheldUntilExit(command)
Whether a command's output reaches the tool only when the whole pipeline
ends — a pipe into tail, grep, sort, wc and friends, which
block-buffer when stdout is not a terminal.
It matters only in combination with an overrun: a command stopped at the tool's ceiling normally hands back everything it printed, but behind one of these it printed nothing, so the whole budget is spent for no information. Measured across six real agent runs: ten such commands, every one of them piped, 49 minutes returning a few hundred bytes each.
function outputIsWithheldUntilExit(command: string): boolean
command— The shell command.
Returns: True when a kill would return no useful output.
outputRedirectTarget(command)
The file a command sends its own output to, when it redirects it (> out.txt,
>> log, &> run.log, | tee [-a] file), or null. A background command that does
this leaves its task log empty, so the log is the wrong place to look and an empty
log proves nothing (X1 flash-m29).
Which redirect counts is decided by the command's own shell structure, from the same
parse every other command reader uses: a redirect written in a BACKGROUNDED statement
(… &) or inside a ( … ) / { … } group writes that process's or group's output, not
the command's. The platform's own preview launch is exactly that — the preview server is
started with >/tmp/mol-preview-4173.log 2>&1 & inside the build/serve prelude, and the
suite's foreground command follows it — so a reader that took the last > in the whole
text told a run its output went to the PREVIEW's log (X1 flash-m136 +18:31).
The target comes back without its shell quotes: > '/tmp/x.log' is /tmp/x.log, and
the callers resolve a relative one against the command's cwd.
function outputRedirectTarget(command: string): string | null
command— The shell command.
Returns: The target, or null (/dev/null and &1/&2 are not files).
streamingBackgroundCommand(command)
Rewrite a command about to run in the background so its log grows while it
runs. Two shapes left the log empty until exit, and an empty log is what
wait_for_task reads as "stuck", so working suites were reported stuck and
killed (X1 run flash-m15 lost 20 of its 60 minutes this way, 2026-10-15):
- a trailing
| tail -N, which holds everything back until the command ends. It is dropped:wait_for_taskalready returns the end of the log. - any output-only filter piped after a SUITE runner (
| grep … | tail -20), which hides the failing test's own error and leaves the pipeline's exit status — the filter's — as the run's. Every such stage is dropped (stripTrailingSuiteFilters);wait_for_taskalready returns the end of the full log, and a run whose tally shows a failure must never report 0 (xr-molecule-flash-m101 +25:54). playwright testwith no--reporter, which runs the config's reporter — oftenhtml, which prints nothing per test.--reporter=listis added when the playwright run is the command's last step, piped or not: it writes each result as it finishes, and it is the shape the platform reads a background suite back in.
The run ends up with exactly ONE reporter that prints each failure — list. A run that names
any other printing reporter has it REPLACED, never joined: two of them write every test result
into the log twice, which on X1 flash-m83 (2026-10-16) made a --reporter=list,line run (its
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/utilities.d.ts.)
function streamingBackgroundCommand(command: string): { command: string; changes: string[] }
command— The command as the executor sent it.
Returns: The command to run and one sentence per change made (empty when none).
isSuiteRunCommand(command)
Whether a command runs a whole test suite — a Playwright, Vitest or Hurl run — rather than a build, an install or a one-off script.
A command that invokes a runner only to print is not one of these
(commandRunsNoTests): npx playwright test --list prints the tests it would run and
exits, and npx playwright install, vitest list and hurl --help start no test either.
function isSuiteRunCommand(command: string): boolean
command— The shell command.
Returns: True when the command starts one of those runners.
withoutSuiteTimeout(command)
The command with the timeout <N> wrapper lifted off the statement that runs the suite, and the
bound that was lifted.
Used where the platform moves a run to the background: the executor wrote the bound to fit this
tool's foreground ceiling, and in the background that ceiling no longer applies — the task is
bounded by wait_for_task and the turn's deadline — so the prefix only kills the walk
(withoutTimeoutWrapper; X1 xr-molecule-flash-m143 +24:45).
function withoutSuiteTimeout(command: string): { command: string; seconds: number | null }
command— The shell command.
Returns: The rewritten command, and the largest lifted bound (null when nothing was lifted).
isPlaywrightRunCommand(command)
Whether a command starts a Playwright run — a playwright test in command position, in a form
that runs something.
npx playwright test --list matched every \bplaywright\s+test\b test in the platform and was
then treated as a run: held (flash-m79's config check), backgrounded for a run that outlasts
the tool's ceiling, and rewritten with --reporter. A list takes two seconds, prints its lines
and exits, so it is none of those; commandRunsNoTests is what decides.
A playwright test named as an argument is not a run either (head -30 run-tests.sh,
grep -n "playwright test" e2e/README.md): the command that runs is the reader.
function isPlaywrightRunCommand(command: string): boolean
command— The shell command.
Returns: True when the command starts a Playwright run.
stripTrailingSuiteFilters(command)
Drop the output-only filters piped after the command's suite runner.
A filter after the runner is not "the end of the log" the executor asked for: it hides the
failing test's own error (it usually matches none of ✘|passed|failed), and the pipeline's exit
status becomes the FILTER's, so … 2>&1 | grep -E "✘|passed|failed|skipped" | tail -20 came back
exitCode: 0 for a run with a failure in it (xr-molecule-flash-m101 +25:54). The tool returns
the end of the full log anyway, so the filter only costs diagnosability.
Only a contiguous suffix of the stages after the runner is cut, and only stages the parse proves
are output-only (isOutputOnlyFilter): a | tee or | xargs keeps its place, and a
caller that reports the run's exit code must then treat it as unknown
(suiteRunPipedAfterRunner).
A statement after the run's own pipeline (; echo done, && echo ok) does not protect the
filters before it: the filter is thrown away and the trailing statement is kept, so the log the
run writes still holds every failure (xr-molecule-flash-m121 — a | tail -5; echo done kept its
tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
that command back).
function stripTrailingSuiteFilters(command: string): SuiteFilterStrip
command— The shell command.
Returns: The rewritten command and the stages dropped.
suiteRunPipelinePlacement(command)
Where the suite runner's exit code comes from: the runner itself, or a stage piped after it.
suiteRunPipedAfterRunner's one question, with the honest third answer kept apart. The
code a finished run reports is its LAST pipeline stage's, so a stage after the runner makes the
code the stage's — but only when such a stage EXISTS. The platform used to report "piped" for
any run whose statements it could not place in the text, which one heredoc body was enough to
cause: on X1 xr-molecule-flash-m105 (+08:12) a config written in the same exec_command made
the run come back with no exit code at all, and the note under it claimed a stage after the
runner that was not there. The parse now places every statement (locatedCommandSegments), so
"unplaced" is the rare, named case rather than a guess dressed as an answer.
function suiteRunPipelinePlacement(command: string): SuiteRunPipelinePlacement
command— The shell command, after the streaming rewrite when one was applied.
Returns: Whether a stage follows the runner, and whether the run could be placed at all.
suiteRunPipedAfterRunner(command)
Whether the command's last step runs a suite whose output still passes through a stage after
the runner — one stripTrailingSuiteFilters could not prove is output-only (| tee log,
| xargs …). The pipeline's exit status is that stage's, not the run's.
function suiteRunPipedAfterRunner(command: string): boolean
command— The shell command, after the streaming rewrite when one was applied.
Returns: True when a pipe this tool must not drop still follows the suite runner.
suiteRunPriorityLaunch(command, launch)
Start a background suite run at lower CPU and IO priority, so the sandbox it shares with everything else stays responsive to what the executor does next.
A whole-suite Playwright run saturates the one container the app's Vite server, its API process and every other command also live in: on X1 flash-m75 the model backgrounded a 139-test run at +36:23 and the two-word shell command it sent next — tick its plan file — returned nothing for 2 min 40 s, measured on the chat payload and on the live-stream follower independently. The backgrounding note is right that the executor should go do other work; what it cannot promise is that the sandbox will let it.
nice -n 10 lowers the run's CPU share and ionice -c3 its IO class. Both
are PROBED in the sandbox by RUNNING them, not with command -v: a syscall a
container's seccomp policy refuses leaves -v passing while the real run
fails. Each is applied only if its probe passed, so a sandbox with neither,
or with only one, runs the suite exactly as it did before. Playwright's own
workers are children of this shell and inherit both (niceness and IO class
are inherited across fork/exec).
Only a command the caller has already detached reaches this: foreground commands are never touched.
function suiteRunPriorityLaunch(command: string, launch: string): { launch: string; note: string }
command— The command as the executor sent it; the suite detection reads this.launch— The shell line that would start it, with no prefix of its own.
Returns: The line to run, and one sentence for the tool note ('' when unchanged).
checkBlockedCommand(command)
Check if a command is blocked for security reasons. Returns error message or null if allowed.
function checkBlockedCommand(command: string): string | null
command— Shell command string proposed for execution.
Returns: A human-readable block reason, or null when the command is allowed.
resolvePath(path, projectRoot)
Normalize a path to be absolute within the project root. Empty string and '/' both resolve to projectRoot. Rejects paths that escape via traversal or absolute paths outside root.
function resolvePath(path: string, projectRoot: string): string
path— Relative or absolute path inside the workspace.projectRoot— Absolute filesystem root for the active project.
Returns: A normalized absolute path confined to projectRoot.
isOutsideRoot(path, projectRoot)
Whether a tool path points outside the project root once normalized (an
absolute path elsewhere, or a .. that climbs out). resolvePath
clamps such a path to the root; a file tool must refuse it instead of
answering about the root — a list_files /tmp/x used to return the
workspace listing as if it were /tmp/x (X1 runs sonnet-m17 and sol-m05,
2026-10-14).
function isOutsideRoot(path: string, projectRoot: string): boolean
path— The path the tool was given.projectRoot— The project root.
Returns: True when the path leaves the root.
isValidGlob(pattern)
Validate that a glob/include pattern is safe (no shell metacharacters that could
inject). Allows alphanumeric, * ? . _ - / and the bracket/paren glob chars [] ().
The brackets/parens matter for real frameworks: Next.js App Router names route
directories [id], [...slug], (group), [[...optional]], so without them the
executor cannot find_files/search_files its own routes on any Next.js project — a
hard block observed on live imports. They are injection-safe here because every caller
passes the pattern through shellQuote before it reaches find -name/grep --include,
where inside single quotes []() are literal (a subshell (...) only starts UNquoted);
to the glob engine [abc] is a normal character class. The genuinely dangerous
metacharacters (; | & $ \ > < \n` space) remain disallowed.
function isValidGlob(pattern: string): boolean
pattern— User-supplied glob fragment for search/list operations.
Returns: true when the pattern contains only allowed characters.
findPatternArgs(pattern)
The find predicate for a find_files pattern.
find -name compares a pattern against a file's BASENAME, so a pattern
containing a slash (helpers/*, e2e/*.spec.ts, src + ** + /App.tsx)
can never match and the tool answers [] (xr-molecule-flash-m77, on two
calls in one run). Such a pattern is matched with -path instead: an fnmatch
glob over the whole printed path, whose * crosses / — the rule
.gitignore uses, and the reason a bare helpers/* means "a helpers/
directory anywhere below the root". The pattern is anchored with a leading
* then / ("any parent") so the search root — an absolute
/workspace/<app> in a sandbox — need not be spelled out.
A doubled-star directory segment keeps its usual meaning of ZERO or more
directories; -path alone would demand at least one, so the pattern is also
emitted with each such segment removed.
function findPatternArgs(pattern: string): string
pattern— A pattern already accepted byisValidGlob.
Returns: A find predicate: -name '…' for a bare name, else one or more -path '…' alternatives, ORed inside parens when there is more than one.
findPathArgs(pattern)
The -path alternatives of findPatternArgs, for a pattern whose
directory the caller has already rooted the search at.
find_files narrows the search root itself when the pattern spells its own
directory out (see literalDirPrefix) — node_modules plus
@molecule/api-resource-user/dist/*.d.ts searched that one directory. The
remainder (*.d.ts) must then keep the -path form rather than collapsing to
-name: * crosses / in an fnmatch glob, so the -path form still finds a
declaration file in a dist/utilities/ subdirectory, which -name '*.d.ts'
would drop (a silent partial answer, which is worse than the timeout it
replaced).
function findPathArgs(pattern: string): string
pattern— The pattern's remainder, with a/in it or not.
Returns: One or more -path '…' alternatives, ORed inside parens when there is more than one.
literalDirPrefix(pattern)
The directory a GLOB pattern spells out, when it spells one out: everything
before the pattern's first glob character, trimmed back to the last /.
A pattern is matched against the whole path with a leading "any parent"
anchor (findPatternArgs), so find visits every entry below the
search root. Handed /workspace/node_modules and
@molecule/api-resource-user/dist/*.d.ts — a directory named outright —
find walked the sandbox's entire dependency tree and hit the tool's 10 s
deadline without printing a result (xr-molecule-flash-m94: the one directory
the model meant answered a list_files in 0 s). The literal prefix is that
directory, and rooting the search at it is what keeps the call instant.
A BARE first segment (helpers/*, bonds/*, e2e/*.spec.ts) is NOT one:
helpers/* means "a helpers/ directory anywhere below the root", which the
tool documents and a live run relied on (xr-molecule-flash-m77 — a path
pattern is not anchored to one depth). Rooting those at
<root>/helpers would answer with a SUBSET, silently: the nested
api/src/helpers/ matches would disappear, and a missing file reads as
"no such file". So a prefix is returned only when it names a path of two or
more segments (@molecule/api-resource-user/dist), which is the shape that
is a directory rather than a directory name.
function literalDirPrefix(pattern: string): string | null
pattern— A pattern already accepted byisValidGlob.
Returns: The literal directory prefix, without a trailing slash, or null when the pattern has no glob character, no /, or only a bare name before one.
isCommandTimeout(error)
Whether a backend error is the sandbox's own command deadline.
The E2B SDK answers a timed-out exec with its generic text —
[deadline_exceeded] the operation timed out: … It can be modified by passing 'timeoutMs' when making the request. Use '0' to disable the timeout. — which
names an option the executor cannot set and says nothing about the search
that died. Handing that sentence to the model is what xr-molecule-flash-m94
read after find_files spent 10 s walking node_modules; the tool answers
the deadline in its own words instead (see searchTimeoutError).
function isCommandTimeout(error: unknown): boolean
error— The error the backend threw.
Returns: True when the error reads as a timeout or a deadline.
searchTimeoutError(tool, searched, pattern, seconds, hint)
The answer a search tool gives when the backend's deadline fires: what was
searched, for what, how long the tool allowed, and the two ways to get an
answer — a narrower path, or a listing. Never the vendor's timeoutMs
sentence (see isCommandTimeout).
function searchTimeoutError(
tool: string,
searched: string,
pattern: string,
seconds: number,
hint: string,
): string
tool— The tool that timed out (find_files,search_files).searched— The directory the search actually covered.pattern— The pattern it was searching for.seconds— The tool's own limit, in seconds.hint— What to do differently — the narrower target or the listing tool.
Returns: The message's text.
pathArgError(path, tool)
Validate a file tool's path argument is a non-empty string. A weak model
sometimes omits it or passes a non-string, which would otherwise crash
resolvePath (path.replace on undefined) with the cryptic, unactionable
"Cannot read properties of undefined (reading 'replace')" — wasting executor
turns. Returns an actionable message, or null when the path is usable.
function pathArgError(path: unknown, tool: string): string | null
path— The rawpathargument from the tool input.tool— The tool name, for the error message (e.g. 'read_file').
Returns: An actionable error string, or null when path is a non-empty string.
emptyCallError(input, tool)
The error for a file tool called with NO arguments at all — edit_file {}, the
shape a truncated provider stream leaves behind.
The path message is the wrong reading of it: "requires a non-empty path
argument" says the call was understood and the path was left out, so the model
retries the call it just made instead of sending the whole call again (x0
flash-m83 +29:32: edit_file {}, one round lost). An empty object and a
missing path are different failures and deserve different sentences.
function emptyCallError(input: unknown, tool: string): string | null
input— The raw tool input.tool— The tool name, for the message.
Returns: The truncation message, or null when the call carried any argument.
directoryReadHint(message, path)
Detect a "read/edit targeted a directory, not a file" failure from a backend
error message (local fs EISDIR or the sandbox's cat: X: Is a directory),
and return an actionable message steering the model to list_files. Returns
null when the error is not a directory error.
function directoryReadHint(message: string, path: string): string | null
message— The backend error message.path— The resolved path that was targeted.
Returns: An actionable directory-error string, or null.
sliceLines(content, window)
Return a line window of content, or null when no window was asked for.
read_file returned whole files only, so an executor that wanted part of one
shelled out: across six real agent runs, 260 of 377 shell-inspection commands
(69%) were head/tail/sed -n windowing a file the tool could have
windowed itself. offset is 1-based, matching the line numbers every other
tool reports and what grep -n prints.
function sliceLines(
content: string,
window?: { offset?: unknown; limit?: unknown },
): { text: string; offset: number; lines: number; totalLines: number; truncated: boolean } | null
content— The file's full text.window— The requestedoffset(1-based) andlimit(line count).
Returns: The window plus its position, or null when neither was given.
serializedChars(text)
A file's text as the request counts it: the length of its JSON string.
JSON escaping (\n → \\n, " → \") costs characters, and the request
side counts the SERIALIZED tool result. A window of 59,000 characters of
source — 1,200 lines — arrives as ~60,200 characters of JSON, over the
ceiling it was sized for, so a window is measured with this rather than by
its raw text length.
function serializedChars(text: string): number
text— The text.
Returns: Its serialized length, without the surrounding quotes.
linesWithinSerialized(content, chars)
How many leading lines of a file fit inside a budget of SERIALIZED chars,
the way read_file sizes a window (see serializedChars).
function linesWithinSerialized(content: string, chars: number): number
content— The file's full text.chars— The budget in serialized chars.
Returns: The line count, at least one.
looksLikeHtml(text)
Whether text is an HTML document (a doctype or an <html root tag).
function looksLikeHtml(text: string): boolean
htmlOutline(text, label)
An OUTLINE of an HTML document: its head's tags and the tags that open each body section, one per line, with line numbers.
Line-based on purpose — a parser would be a dependency and an attack surface
for a read that only has to be navigable. A line is shown when it starts with
< after trimming, which is what keeps the inside of a <style> block, a
<script> body or a prerendered text section out of the outline while keeping
every tag the document is built from. Long lines (a minified tag, a data URI)
are cut, and the whole outline is capped, so a pathological document cannot
turn one read into another wall of text.
function htmlOutline(text: string, label?: string): string | null
text— The document's full text.label— What to call the document in the outline's header (its path).
Returns: The outline — or null when text is not an HTML document.
truncate(s, maxLength)
Truncate a string to a max length with a truncation notice.
function truncate(s: string, maxLength: number): string
s— Arbitrary text to bound in size.maxLength— Maximum number of characters to retain before truncating.
Returns: Either the original string or a shortened copy with a trailing notice.
truncateMiddle(s, maxLength)
Truncate keeping BOTH the head and the tail, eliding the middle — for command
output (build / test / migration / install logs). Plain head truncation
(truncate) drops the TAIL, which is exactly where a failing command puts
the reason: the npm ERR! line, the test-failure summary (1 failed, 240 passed), the migration stack trace. When that is cut, the executor sees only
passing progress and can't tell WHY the command failed — a self-inflicted error
then survives every fix round. The head still shows what ran and the first
errors; the split is weighted toward the tail since the summary lives there.
No-op when s already fits.
function truncateMiddle(s: string, maxLength: number): string
s— Arbitrary text (typically stdout/stderr) to bound in size.maxLength— Maximum characters to retain (excluding the elision notice).
Returns: The original string, or head + an elision notice + tail.
failureBlocks(s)
The numbered failure blocks of a Playwright report ( 1) [chromium] › file:line › title
up to the next block or the tally).
function failureBlocks(s: string): string[]
s— Test-runner output.
Returns: Each block, in order (empty when there are none).
runningOutputSliceCommand(log, own)
The shell command whose stdout is the interesting part of a log that is still being written:
the failure lines so far (capped at RUNNING_FAILURE_LINES), then the end of the log.
A Playwright list run prints one line per test as it finishes, so the LAST bytes of a running
log are its least informative part — in X1 flash-m61 the last 3 000 bytes of a 139-test walk
were the skip list, and all 61 failures were above them, which cost the run the grep it had to
make anyway. A background run also carries each failure's own error as it fails
(PLAYWRIGHT_FAILURES_INLINE_ENV), and RUNNING_FAILURE_LINE_RE already matches
those blocks' headers and their Error: lines, so this slice shows the causes too.
function runningOutputSliceCommand(log: string, own?: string): string
log— The task log path.own— A file the command redirects its own output to, if any.
Returns: The shell command, safe to hand to the execution backend as-is.
runningCauseCommand(log, own)
The shell command whose stdout is enough of a running log to tell whether one cause is
repeating: its tail, REPEATING_CAUSE_TAIL_BYTES of it.
runningOutputSliceCommand is the wrong read for this: it keeps only the FIRST 40
failure lines plus 1 000 bytes, so on a run whose failures print a body each — which is what
a line reporter does, and the only form that names the cause as an error message rather than
a duration — three bodies do not fit, and the repeat is invisible. This read is not shown to
anyone; it is parsed.
function runningCauseCommand(log: string, own?: string): string
log— The task log path.own— A file the command redirects its own output to, if any.
Returns: The shell command, safe to hand to the execution backend as-is.
repeatingFailure(log)
The cause repeating among a running suite's failures, or null.
Playwright's list reporter prints one line per test as it finishes — its mark, its title
path and how long it ran — and, on a run the platform started in the background, each failure's
own error with it (PLAYWRIGHT_FAILURES_INLINE_ENV); a list run with no error bodies in
its log (an older Playwright, a log the bodies have been cut from) carries only the duration as
a signature. Where the log carries the failure bodies (either inline, or in a finished run's
summary) the signature is the
error itself — failureMessage: the error class plus the locator or the values that
differ — which is stronger (it needs no assumption about how long a failure should take), so
the message is looked for first and the duration is then left with the tests whose bodies the
log does NOT carry.
Either way the point is the same: a suite whose failures repeat one cause spends its whole
runtime proving that one cause, and the executor watching it can be told so at the third
failure instead of at the last. X1 flash-m76 walked 15 tests in 4.0 minutes where 15.6 s
each was the test timeout and one missing navigate('/') was the cause of all of them.
The quorum is of DISTINCT tests — REPEATING_FAILURE_MIN of them — because a log that
prints each failure twice (a second reporter, a retry) had X1 flash-m83 told that four of its
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/utilities.d.ts.)
function repeatingFailure(log: string): RepeatingFailure | null
log— The log so far.
Returns: The repeating cause, or null when no signature has REPEATING_FAILURE_MIN distinct tests behind it.
specFilesSelected(command)
How many test FILES a Playwright run's own command narrows it to, or null when it selects the whole suite (or its selection cannot be read).
universalCause's file threshold is written for a whole-suite run, which a narrowed run
can never reach. xr-molecule-flash-m119 (+42:08) re-ran ONE spec file against a hand-started
vite preview the browser could not reach, and every test in it failed page.goto: Timeout —
but one file is below UNIVERSAL_CAUSE_FILES, so the wait sat out its entire 280 s
budget on a run that had already measured all it could.
The command is read through commandSegments, so a playwright test named as an
argument is not a run. A PLAYWRIGHT_VALUE_FLAGS flag written without = takes the next
word as its value, so a --config app/e2e-suite.config.ts is never counted as a spec file; a
directory or a bare substring filter names files the command does not list, which answers null
(the whole-suite threshold) rather than a count of the wrong files.
function specFilesSelected(command: string): number | null
command— The run's command.
Returns: The number of distinct test files it names, or null when it names none (the whole suite) or the selection cannot be told.
universalCause(log, found, command)
Whether a repeating cause is failing the whole run — the one case where stopping it is the right call.
The test is: the cause accounts for every failure the log names, it has hit the spec-file
threshold, and nothing has passed since its first failure. The threshold is
UNIVERSAL_CAUSE_FILES for a whole-suite run but the number of files the command
SELECTED for a narrowed one (specFilesSelected): a one-file run can never span three,
and xr-molecule-flash-m119 (+42:08) waited out its whole budget on exactly that shape. An app
that is not up, a 5xx on every request, or a selector that is missing on every page all look
like that, and every remaining test will fail the same way — so the run's remaining minutes
measure nothing. A CONNECTION-class cause (CONNECTION_CAUSE) needs no file span at all:
three tests that could not reach the app, with nothing passing since, is the app-unreachable
case however few files the run walks.
Anything less is NOT this: a cause that stops at one file of a multi-file run, or that leaves other tests passing, or that shares the run with failures of another shape, leaves the run with something to say, and the run is the only thing that will say it. X1 flash-m83 (2026-10-16) stopped eight local runs at their first repeating cause over 14.5 minutes and never finished one; three of its deploys each fixed a single cause that a finished run would have listed alongside the others.
function universalCause(
log: string,
found: RepeatingFailure | null,
command?: string | null,
): UniversalCause | null
log— The log so far.found— The repeating causerepeatingFailurefound in it, or null when no two failures share a signature (a page that renders nothing fails each spec at its own first locator —emptyPageCausestill reads that run).command— How the run was started, when the caller knows it: its spec-file selection scales the file threshold (specFilesSelected).
Returns: The cause's count and files, or null when the run is not failing whole.
earlyReturnCause(found, universal)
The ONE finding that ends a wait early, and the only one a note may hand a stop command over for: a PROVEN cause that is failing the whole run.
Both halves are load-bearing, each of them learned from a run that lost minutes to the other.
- Failing the WHOLE run is
universalCause: every failure the log names, across the spec-file threshold —UNIVERSAL_CAUSE_FILES, or the files a narrowed run's own command selects, orCONNECTION_CAUSE_TESTStests of an app that never answered — with nothing passing since the first. That is the dont-stop-runs rule. X1 flash-m83 (2026-10-16) stopped eight local runs at their first repeating cause in 14.5 minutes and never finished one, and a run that still has other causes left to name is the only thing that will name them. - A PROVEN cause is an error repeated VERBATIM. A shared DURATION is a LEAD, never a cause:
a suite whose config sets a timeout spends that timeout on every failure that waits for
anything, so three unrelated tests timing out together look exactly like one broken run
(
~7 sis a property of the CONFIG, not of the cause —normalizedSignature). X1 xr-molecule-flash-m99 (2026-10-16) was toldstatus: 'repeating_failure'twice on that shape — threeurl-navigation.spec.tstests at ~16.2 s, onetimeout: 15_000plus teardown — read the early return as "stop waiting, edit now", and voided a run that was still walking;
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/utilities.d.ts.)
function earlyReturnCause(
found: RepeatingFailure | null,
universal: UniversalCause | null | undefined,
): UniversalCause | null
found— WhatrepeatingFailurefound mid-run, or null when no signature repeats.universal— WhatuniversalCausefound for it, if anything.
Returns: The cause to stop the run over, or null — in which case nothing may be stopped, the wait keeps going, and the finding rides the run's result as a note.
matchingExcludedTitles(heading, excluded)
The titles a request excludes that a test's heading names — the ONE matcher for "is this failure one the request does not count?".
The request quotes a test's own title (… except "should require login to post comment",
extracted by molecule-dev's excludedTestTitles) while a run's heading carries more of it
(comments.spec.ts › Comments › should require login to post comment), so containment is the
comparison. Whitespace is normalised and case is ignored: a brief may wrap the title across a
line (xr-molecule-flash-m44), and the reporter's own casing never has to match the brief's.
Two readers used to answer this separately — the causes note (which therefore asked the executor to fix a test the same result said not to) and molecule-dev's exclusion notes — and xr-molecule-flash-m101 paid for the gap (+12:00: a turn and an edit spent on the excluded test).
function matchingExcludedTitles(heading: string, excluded: readonly string[]): string[]
heading— The text to match against: a test's title, its wholefile › describe › titlepath, or (for a note that only has the run's output) the run's text.excluded— The titles the request carves out, if the caller knows them.
Returns: The excluded titles this heading names, in the request's order.
failuresByCause(log, excluded)
Every failure a run names, grouped by the cause behind it — one group per cause, with the tests it hit.
The point is that ONE run yields EVERY cause. A Playwright report prints its failures one after another with no statement of which of them share a cause, so a run of 19 failures is 19 bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a single cause, two of them found by runs stopped before they walked the rest.
The signature is the cause itself where the log carries the failures' bodies (a finished run, a
background list run — whose failures print as they fail — or a line reporter) —
failureMessage: the error class plus the locator or the values that differ — and how long
they took where it does not, which is why a duration group is labelled as a duration and not as a
proven cause. A test the log printed twice, or failed twice, is one test.
A test the request EXCLUDES by name is not a cause to fix: its failure does not count for
this request (matchingExcludedTitles), so it is dropped here rather than handed to the
executor as work — a result that names it beside "it does not count" is the contradiction
xr-molecule-flash-m101 filed (+12:00: the model spent a turn and an edit on the excluded test,
on a page that was passing).
function failuresByCause(log: string, excluded?: readonly string[]): FailureCause[]
log— The log of a finished or running run.excluded— The test titles the request carves out, if the caller knows them.
Returns: The causes, the one with the most tests first.
failureCausesNote(log, excluded, run)
The one-line-per-cause summary of a finished run's failures, or null when the run named fewer than two of them.
This is what makes a run worth finishing: every cause the run found, in the result the executor reads, so one round of fixes covers all of them rather than the one cause a run stopped early happened to reach.
A test the request excludes is not one of the causes — it is dropped and named once, at the
end, so a result never asks to fix a test it also says to ignore. A test the runner itself
declared flaky (it failed once and passed on its config retry) is not a cause either: it is
named apart from them, once, as what the tally called it (flakyTestsDeclared). Where
the caller knows how the run was started, the note also hands over the narrowed re-run of
exactly those tests (rerunOfRunningSuite) instead of leaving the executor to re-run the
whole suite.
A run whose failures are ALL NO_BASE_URL_ERROR is not a set of causes at all: it is one
configuration failure, said in one line (noBaseUrlConfigNote) rather than a list that
reads as ten defects in the app.
function failureCausesNote(
log: string,
excluded?: readonly string[],
run?: { command: string; cwd?: string; log?: string; config?: string | null },
pageErrors?: readonly CausePageErrors[],
): string | null
log— The finished run's output.excluded— The test titles the request carves out, if the caller knows them.run— How the run was started, when the caller knows it: the narrowed re-run is built fromcommand/cwd/config, andlog(the path the output was written to) is what the "left out" sentence greps when more causes exist than the result has room for.
Returns: The note, or null.
rerunOfRunningSuite(run, locations, excluded)
The command that re-runs the tests at locations the way the run IN FLIGHT ran them: the same
invocation, in the same directory, narrowed to those tests — or null when the running command
is not a Playwright run this can narrow.
The locations are the paths the run's own log printed, which are relative to the directory
that run started in, so the composer resolves them there (scopedRerunCommand): the
command names that directory unless it already changes into one of its own, because the
executor's next command starts wherever it starts. The returned command is for AFTER the run
ends — it is a second run against the same app and database, and that is exactly what is
refused while the run walks (suiteRunInTheWay).
function rerunOfRunningSuite(
run: { command: string; cwd?: string; config?: string | null },
locations: readonly string[],
excluded?: readonly string[],
): string | null
run— The run in flight: the command it was started with, as it was run, the directory that command ran in, and the config it loaded when the caller knows it.locations— The failingfile:lines to re-run, as the run's log printed them.excluded— The test titles the request carves out: the re-run inverts them out (--grep-invert) so it cannot report them again.
Returns: The re-run command, or null when there is no suite to narrow.
repeatingFailureNote(found, o)
The sentence that names a cause repeating among a running suite's failures, mid-run.
Kept here, beside repeatingFailure, so the two stay in step: every word below is about
what that function could actually see in the log — including the count, which is of DISTINCT
tests, so the note never names one or two tests as a cause.
The note names the cause early so the executor can START FIXING while the run keeps walking,
and it hands a stop command over only when earlyReturnCause says so — the SAME
predicate the wait uses to decide whether to return early, so a finding the wait keeps waiting
on is a finding this note never asks to stop for. X1 flash-m76 (2026-10-16) spent 4.0 minutes
walking 15 tests whose one cause was certain at the third, 47 s in, and the note answers
exactly that; X1 flash-m83 then stopped eight runs at their first repeating cause in 14.5
minutes and never finished one, so the note must not say to stop when the run still has other
causes to name. X1 xr-molecule-flash-m99 (2026-10-16) is the third shape: a shared duration is
not a cause at all, so a lead reaches the executor as a note on a result it is still waiting
for — never as a verdict with a stop command in it.
The run is STILL WALKING while this note is written — both callers produce it mid-run — so a
re-run of one of its tests is a second run against the same app and database, and the platform
refuses that (suiteRunInTheWay, the same rule its overlap hold answers with):
(description truncated — read the full text in node_modules/@molecule/api-ai-tools/dist/utilities.d.ts.)
function repeatingFailureNote(
found: RepeatingFailure,
o: {
taskId: string
log: string
stop: string
universal?: UniversalCause | null
run?: { command: string; cwd?: string }
editsInvalidateRun?: boolean
},
): string
found— WhatrepeatingFailurefound.o— The run's facts: itstaskId, itslogpath,stop— the shell command that stops it, printed only for anearlyReturnCause—universal, whatuniversalCausefound (absent means the run is not failing whole),run, how the run was started (rerunOfRunningSuite) when the caller knows it, andeditsInvalidateRun, true when the run measures the sandbox's own app (a local run, where an edit now changes what the suite is testing and its result stops counting).
Returns: The note.
whitespaceTolerantReplace(content, oldString, newString)
Attempt a whitespace-tolerant replacement when an exact old_string match
failed. Finds a contiguous run of lines in content whose per-line
whitespace-normalized form (runs of whitespace collapsed to one space, then
trimmed) equals the normalized oldString lines, and replaces that run with
newString verbatim. Applies ONLY when exactly one such run exists —
uniqueness keeps it safe; an ambiguous (or zero) match is refused (returns
null) so the caller falls back to its existing error path.
This rescues the most common edit_file failure: a (weak) executor reproduces the target text correctly but with different indentation or trailing whitespace, which would otherwise bounce it into a re-read/retry loop — the single biggest source of wasted edit turns.
function whitespaceTolerantReplace(
content: string,
oldString: string,
newString: string,
): string | null
content— Current file content.oldString— The search text (an exact match has already failed).newString— The replacement text, applied verbatim.
Returns: The new content if a unique fuzzy run matched, else null.
parseCheckCommand(path)
The shell command that parses one file and exits non-zero with the error
when it does not parse: node --check for JavaScript, JSON.parse for
JSON, esbuild's transform (present wherever Vite is) for TypeScript.
function parseCheckCommand(path: string): string | null
path— The file's absolute path.
Returns: The command, or null when the file type has no cheap parser.
parseCheckProblem(result)
What a failed parse check says to the executor, or null when the check passed or could not run (no parser available is not a syntax error).
function parseCheckProblem(result: {
stdout: string
stderr: string
exitCode: number
}): string | null
result— The check command's result.
Returns: The message for the tool result, or null.
fusedLine(content, newString)
The line a replacement fused when it dropped the newline its old_string
ended with: new_string's text is followed on the same line by what used to
be the next line. Read off the file as it now is, so the line shown is the
line the parser is actually complaining about.
function fusedLine(content: string, newString: string): { line: number; text: string } | null
content— The file's content after the edit.newString— A replacement'snew_string, whoseold_stringended with a newline.
Returns: The fused line's 1-based number and its text, or null when the replacement's text is not followed by more of the same line (it ends the file, the following newline survived, or another replacement rewrote it).
Constants
CUT_AT_BUDGET
The state a result carries when a budget stopped its command: a run that did not finish is not a run that passed, and no reader may treat it as one.
const CUT_AT_BUDGET: 'cut_at_budget'
TOOL_SCHEMAS
Canonical tool schemas shared by the agent runtime and documentation.
const TOOL_SCHEMAS: Record<string, ToolSchema>
MIN_PEEK_SLEEP_SECONDS
The shortest leading sleep the platform drops: below it the wall clock it costs is noise, and a
short settle (sleep 2; curl …) is a legitimate thing to run.
const MIN_PEEK_SLEEP_SECONDS: 5
SPLIT_WORK_ADVICE
The closing advice on an over-budget message for a command that is not a test suite: the work it was chaining is what to split.
const SPLIT_WORK_ADVICE: string
SUITE_OVER_BUDGET_ADVICE
The closing advice on an over-budget message for a command that IS a suite run
(Playwright, Vitest, a hurl run — isSuiteRunCommand): its files are not
a bigger unit to break up.
X1 flash-m36 asked for this half and the message kept saying "run a smaller unit" for two months. It is the wrong direction for a suite, and measurably: a run stopped here reports NO tally, so nothing in the result says which test or request hung — the run has to be sent again in the background (off this budget, its output written to a log that can be read mid-run) or, when a consumer knows the runner's per-request limit, with each request bounded. xr-molecule-flash-m93 (+06:22, 2026-10-16) lost 4.8 minutes to a Hurl run whose ONE unanswered request had hurl's 300 s default per request holding the file, ten seconds past this tool's budget.
const SUITE_OVER_BUDGET_ADVICE: string
MAX_TRACE_ARTIFACTS
How many traces one read reports, newest first.
const MAX_TRACE_ARTIFACTS: 12
MAX_TRACED_REQUESTS
How many requests one test's trace contributes.
const MAX_TRACED_REQUESTS: 6
MAX_TRACED_CONSOLE
How many console lines one test's trace contributes: errors ahead of a session or molecule-package line, both ahead of a plain warning and of the browser's own resource error.
const MAX_TRACED_CONSOLE: 4
MAX_TRACED_ACTIONS
How many of the page's own actions one test's trace contributes, the LAST ones.
const MAX_TRACED_ACTIONS: 40
MAX_TRACED_BODY_CHARS
How much of a 4xx request's sent body (or its response body) the trace read keeps.
const MAX_TRACED_BODY_CHARS: 300
MAX_TRACE_NOTE_TESTS
How many tests the note in a run's result names.
const MAX_TRACE_NOTE_TESTS: 5
MAX_TRACE_NOTE_REQUESTS
How many requests the note names per test.
const MAX_TRACE_NOTE_REQUESTS: 4
TRACE_NOTE_URL_CHARS
How long a URL the note prints, before the middle is cut.
const TRACE_NOTE_URL_CHARS: 96
TRACE_NOTE_SENT_CHARS
How much of the sent body the note prints — enough to see a {"data":…} wrapper.
const TRACE_NOTE_SENT_CHARS: 160
TRACE_NOTE_GOT_CHARS
How much of the response body the note prints — the API's first error line.
const TRACE_NOTE_GOT_CHARS: 120
TRACE_NOTE_LEAD_GOT_CHARS
How much of the FAILING STEP's response body the note prints. Longer than
TRACE_NOTE_GOT_CHARS: it is the one line that answers "what did the click actually get
back", and an app's own body ({"profile":{…}}) can carry the answer past its first clause.
const TRACE_NOTE_LEAD_GOT_CHARS: 200
DIAGNOSIS_TRACE_FLAG
The --trace flag this platform adds, and the only value it adds it with.
const DIAGNOSIS_TRACE_FLAG: '--trace=retain-on-failure'
PLAYWRIGHT_FAILURES_INLINE_ENV
Playwright's switch for printing each failure's own error as that test fails, instead of only in
the summary at the end: list reads it in its constructor (ListReporterOptions.printFailuresInline,
playwright/lib/reporters/list.js — 1.63, the version the toolchain pins), and when it prints
inline the end-of-run repeat is suppressed with it (epilogue(!printFailuresInline)), so one
reporter still prints each failure exactly once.
A background suite is put on list (see streamingBackgroundCommand) and this with it, so
the log names its causes WHILE it walks. Without it the log's only failure lines are ✘ rows:
X1 xr-molecule-flash-m99 (+19:27 → +21:00, 2026-10-16) was told to grep -n -A 20 '<test name>'
the log, got nothing back through four greps, inferred causes from the spec prose instead, and
edited app/src while the run was still walking — which voids the run.
const PLAYWRIGHT_FAILURES_INLINE_ENV: 'PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE'
MAX_READ_SIZE
Max file size for read_file (5MB).
const MAX_READ_SIZE: number
MAX_WRITE_SIZE
Max content size for write_file (10MB).
const MAX_WRITE_SIZE: number
MAX_OUTPUT_SIZE
Max command output size (100KB per stream).
const MAX_OUTPUT_SIZE: number
MAX_SEARCH_CONTEXT_LINES
Most lines of surrounding context search_files will return per match.
Bounded so a broad pattern with a generous context cannot pull a whole tree
into one reply.
const MAX_SEARCH_CONTEXT_LINES: 40
MAX_BATCH_READ_FILES
Most files one batched read_file call returns. The cap exists so a survey
of a large tree cannot swallow the turn's context in one result; every path
past it is named in the result's note and listed with its own error, so
the model continues from there.
const MAX_BATCH_READ_FILES: 25
MAX_READ_CALL_CHARS
The most one read_file RESULT may carry, in chars of its JSON — every file
the call returns, the envelope and the note included. That is how the request
side counts a tool result, so it is how this tool counts one too
(serializedChars).
One tool result is ONE block in the request, and the request side caps a
block: molecule-dev cuts any block over MAX_TAIL_BLOCK_CHARS (60,000) in
place, keeping ~30 KB of each end. A batched read that crossed it lost its
MIDDLE — the cut lands between two "content" values, so whole files fell
out and the note in their place said only "N characters cut here" — while
this tool's own note had promised each of those files that it "fits in one
call, so it is not paged": X0 flash-m81 (2026-10-16) read ten e2e specs in
one call (121,467 chars), lost the middle six, and bought them again in six
further calls (52,908 chars).
So the CALL gets the ceiling the request gives it. Whole files are returned
up to it; the paths that did not fit are named in note and listed in
files with their own error, the shape every other entry has. No file is
ever cut, and no read result reaches the request-side cut. A test in
molecule-dev's context-fit asserts the two ceilings still agree.
const MAX_READ_CALL_CHARS: 60000
READ_RESULT_ENVELOPE_CHARS
Room inside one read result for what is not file content: the path, the
offset/line fields, and the note.
A batch sizes each file against what the call has left
(MAX_READ_CALL_CHARS less this), so the entry that does not fit is
caught here rather than by the request-side cut.
const READ_RESULT_ENVELOPE_CHARS: 1000
MAX_READ_RETURN_CHARS
The most one read_file call returns for ONE file when no window was asked for, in serialized chars: larger files come back as their first window, with a note saying how to read the rest.
MAX_READ_SIZE (5 MB) is a bound on what the tool will open, not on what it
should hand a model: a single batched read that returned a 1.2 MB README
put the next three provider calls at 366k tokens (~330k of them fresh)
against a 120k-token context cap (X0 run x5, call 26). This is the call's
own ceiling (MAX_READ_CALL_CHARS) less the room its envelope takes,
so one read result — one file or twenty-five — fits inside what the request
side carries without being cut.
const MAX_READ_RETURN_CHARS: number
SMALL_FILE_WHOLE_CHARS
A file this small is returned whole on a second window of it (paging it costs a call each).
The same bound as MAX_READ_RETURN_CHARS: the windowed-read note promises that a
file under it "fits in ONE call", and a smaller bound here broke that promise (X1 flash-m67).
const SMALL_FILE_WHOLE_CHARS: number
MAX_HTML_INLINE_CHARS
How large an HTML document must be before a whole-file read is replaced by its
outLINE — much smaller than MAX_READ_RETURN_CHARS, because the cost of
an HTML read is almost never worth it.
An entry document is boilerplate the reader rarely needs in full: meta tags,
stylesheet and preload links, a <style> block, and a handful of <script>
tags. The parts an executor acts on — the body's structure, where a script is
loaded — are all visible in an outline of one line per tag. X0 flash-m77 paid
63,735 chars for one such read: ~60 KB of it was the IDE's own preview bridges,
and the read pushed the transcript past its context cap, so the e2e specs the
executor had just read were trimmed to summaries and it read the whole suite
again — twice (findings, +01:23 and +02:56). The scaffold now keeps those
bridges in separate files, but a template's own head is still ~5-12 KB, and a
static-site or SSR entry can be far more.
The outline is not a truncation: the note beside it names the exact offset/limit that returns the whole document in one call.
const MAX_HTML_INLINE_CHARS: number
MAX_HTML_OUTLINE_LINES
Most lines an HTML outline prints before it stops and says so.
const MAX_HTML_OUTLINE_LINES: 200
MAX_SEARCH_RESULTS
Max search results.
const MAX_SEARCH_RESULTS: 50
MAX_FIND_RESULTS
Max find results.
const MAX_FIND_RESULTS: 100
DEFAULT_SEARCH_EXCLUDED_DIRS
Default directory names search_files/find_files skip — VS Code's
search.exclude + files.exclude defaults (node_modules, bower_components,
VCS dirs) plus the platform's vendored/build dirs. Overridable per consumer
via ToolBuildConfig.searchExcludedDirs (a per-project, user-editable
setting in molecule.dev — keep the APP-SIDE copy in
@molecule/app-ide-react's search types in sync with this list).
const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
'node_modules',
'bower_components',
'.git',
'.svn',
'.hg',
'CVS',
'dist',
'.next',
'.vite',
'molecule',
]
RUNNING_FAILURE_LINES
Failure lines shown from a log that is still being written.
const RUNNING_FAILURE_LINES: 40
RUNNING_TAIL_BYTES
Bytes of a running log's end appended after its failure lines.
const RUNNING_TAIL_BYTES: 1000
RUNNING_FAILURE_LINE_RE
The lines worth showing from a log that is still being written: a failed test's mark or
numbered block header, and a runner's own error lines. Matched with grep -E.
const RUNNING_FAILURE_LINE_RE: '^[[:space:]]*(✘|✗|×|✖|✕|[0-9]+\\)|[0-9]+ (passed|failed|flaky|skipped|did not run)|FAIL|FAILED|ERROR|Error:|error:|Traceback|AssertionError)'
REPEATING_CAUSE_TAIL_BYTES
Bytes of a running log a repeating-cause look reads back.
const REPEATING_CAUSE_TAIL_BYTES: 40000
REPEATING_FAILURE_MIN
DISTINCT tests that must share one signature before a running suite's repeating cause is named. A test the log prints twice, or a test that failed twice, is still one test.
const REPEATING_FAILURE_MIN: 3
REPEATING_FAILURE_LISTED
How many of a repeating cause's failures the note lists before it says "and N more".
const REPEATING_FAILURE_LISTED: 5
TOOL_RESULT_CHARS
The platform's cap on one tool result's text: MAX_TOOL_OUTPUT_CHARS in molecule-dev's
api/src/ai/chat-handler.ts, which cuts a longer string field's MIDDLE. A finished run's
stdout is cut to it, so the cause list must not depend on reading the log back — and must
itself fit inside it, or the cut lands in the list.
const TOOL_RESULT_CHARS: 8000
FAILURE_CAUSES_CHARS
The characters a finished run's cause list may use of TOOL_RESULT_CHARS. The recap
measured by xr-molecule-flash-m102 (+12:43) was 12 failures, 8 causes: one cause line is
~200 chars and ~80 more with its two body lines, so 8 causes is ~2.3 k against the 8 k cap —
the old 5-cause cap withheld 3 causes that the same result had room for.
The rest of the cap is left to the header, the narrowed re-run command and the exclusion line, which ride the same note.
const FAILURE_CAUSES_CHARS: number
FAILURE_CAUSES_LISTED
How many causes a finished run's summary lists at most. The character budget above is the bound that must hold (it is what keeps the note on the surviving side of the tool-result cut); this is the bound past which a list stops being read at all.
const FAILURE_CAUSES_LISTED: 12
FAILURE_CAUSE_TESTS_LISTED
How many of one cause's tests that summary names before it says "and N more".
const FAILURE_CAUSE_TESTS_LISTED: 6
UNIVERSAL_CAUSE_FILES
Distinct spec FILES a repeating cause must span — with every failure sharing it and nothing passing since — before a run is called broken for the whole suite.
One file's failures can be that file's own setup, so a cause confined to one file is never
this. Across UNIVERSAL_CAUSE_FILES files, on the other hand, whatever it is is a
property of the run rather than of a page.
const UNIVERSAL_CAUSE_FILES: 3
EMPTY_PAGE_CAUSE_TESTS
How many tests a run must have walked — with nothing passing since its first failure — before "the page renders nothing" can be its one cause. A handful of failures is a handful of bugs; a page that never painted fails every spec that reaches it, and m141's walk had 36 by +20:30.
const EMPTY_PAGE_CAUSE_TESTS: 15
CONNECTION_CAUSE_TESTS
How many tests a connection-class cause must cover before the run is stopped even when they
are all in one file — see universalCause.
const CONNECTION_CAUSE_TESTS: 3
PARSE_CHECKABLE
Files a parse check can vouch for cheaply right after a write.
const PARSE_CHECKABLE: RegExp
Injection Notes
Requirements
Peer dependencies:
@molecule/api-ai^1.0.1
Runtime Dependencies
-
@molecule/api-ai -
This package only defines the tools — it runs no model loop. Hand them to the bonded AI provider (
chat({ tools, … })) or to an@molecule/api-ai-agentsrun. -
Safety defaults: keep them on for model-driven use.
pathGuards(default true) rejects paths escapingprojectRoot;redactSecrets(default true) masks secret-looking values in file reads and command output. -
exec_commandexecutes REAL shell commands on the backend. Prefer a sandbox backend for untrusted/model-driven work; add environment-specific rules viablockCommand(return an actionable error string to refuse — it is shown to the model verbatim) and enableblockDangerousCommands(default FALSE) where appropriate. -
read_filetakespathsas well aspath. An agent that issues one tool call per model round-trip pays a full round-trip per file, so a project survey costs minutes; batched, the same reads cost one call each 25 files (MAX_BATCH_READ_FILES). One result is capped atMAX_READ_CALL_CHARS, the ceiling the request side puts on a single tool result: whole files are returned up to it and every path past that is named innoteand listed asnot returned, so a file is never handed back in part — the shape that lost the middle of a ten-file batch to the request-side cut (X0 flash-m81). Each entry carries its own content or its own error, so one bad path never fails the batch. -
edit_fileshows the result, so the model need not re-read. A successful edit returnsexcerpt: each edited region as it now reads, with 1-based line numbers (N: text) and a few lines of context, capped at 60 lines in total (what did not fit is named by line range), plustotalLines.write_filereturnstotalLines, andexcerptonly for a small file (up to 40 lines) — a big file is never echoed back. Line numbers count the wayread_filedoes, so they are valid offsets. The excerpt is redacted like a read. The existing fields (ok,replacementsApplied,alreadyApplied,syntaxError) are unchanged. -
A dropped line break and the parse error it causes are reported as ONE thing. When a replacement's
old_stringended with a newline and itsnew_stringdoes not (so the next line is pulled up onto it) and the file then fails its parse check,edit_filereturns a singlesyntaxErrorthat names the replacement, shows the fused line with its number, and says to re-send that replacement with the newline restored — instead of thenewlineNotehint beside an unrelated-looking parse error, which an executor repairs by rewriting the fused line's text and fuses the next one. ThenewlineNotealone is still returned when the file parses. -
A replacement that only changes a line break at its boundary is refused, not applied. When
new_stringisold_stringwith its trailing newline added or removed and nothing else, the edit can only move text across lines — joining the next line onto the replaced text, or pushing the rest of the line down — which is what a weak model does by accident when it repeats a line it means to keep (flash-m28, m55, m62, m89, five times in the m89 run alone). The replacement is skipped, the rest of the batch is applied, and the result carriesskippedNote: one line naming the replacement, what applying it would have done, and how to say it on purpose (include the next line inold_string). The file's own next character decides: text on the same line is a join or a split and is refused; a blank line after it, or the end of the file, means no text crosses lines and the edit is applied as written. -
A parameter under another name is not a broken tool. Every tool's input is normalized first (
cmd→command,file→path, camelCase ⇄ snake_case), and a handler that throws returns an error naming the missing parameter rather than a bare internal message — which a weak executor reads as a broken tool and routes around, taking its verification with it. UseguardAIToolto give tools built elsewhere the same treatment, passingredactErrorso a thrown message is masked by your own redactor. -
Scope the tool set per agent with
include/exclude— a read-only agent should not receivewrite_file/exec_command. -
Outputs are size-capped (
MAX_READ_SIZE,MAX_OUTPUT_SIZE, …) and searches skipDEFAULT_SEARCH_EXCLUDED_DIRS(node_modules, VCS dirs, build output) — passsearchExcludedDirsso every search surface shares the consumer's one setting.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual agent/chat surface and its registered tools, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- A prompt that should trigger a registered tool makes the model INVOKE
it with args matching that tool's
parametersschema (e.g. "read src/index.ts" -> callsread_filewith{ path: 'src/index.ts' }). Confirm the tool'sexecuteactually RAN — its backend side effect / log / the file it touched — not that the model merely narrated calling it. - The tool's returned value flows back into the model and shapes the final answer: the REAL result (the file's actual contents, the command's real stdout/exitCode) appears in the reply, not a plausible hallucination.
- A prompt that needs no tool is answered directly, with no spurious tool call.
- A tool whose
executethrows or returns{ error }(missing file, failing command, blocked path) degrades gracefully — the error is caught and fed back to the model as text, the conversation continues, and nothing crashes the request. - The model can invoke ONLY the tools handed to this run: an
include/exclude-scoped agent (e.g. read-only — nowrite_file/exec_command) cannot call an excluded tool, and a tool name the model invents that was never registered is refused, not executed. - Tool execution is server-side and authorized:
exec_command/write_filerun only on the bonded backend under its guards (pathGuards,symlinkGuards,redactSecrets,blockDangerousCommands/blockCommand) and stay insideprojectRoot. Feed a prompt-injected instruction (a file or message telling the model to read/etc/passwd, escape the workspace, or run a privileged command) and confirm the guard REFUSES it — a user must not be able to trigger, via the model, any action they could not perform directly.