Structure
The stage
Section titled “The stage”Stability: stable.
A stage is one step in a flow. A flow is a numbered folder of stages, and an assembly is the folder that holds the flows.
A stage is a box with one input and one output. Inside are the parts this
document specifies — a before hook, the agent loop, three checks, and a
success or failure hook — and every one of them is optional except the agent
loop.
A stage is a single file or a folder.
01-name.md is a stage defined entirely by one markdown file: frontmatter for
its configuration, body for its prompt — fenced frontmatter and body like any
sentinel, even when the frontmatter is empty
(invariant 42: one grammar, one parser).
01-name/ is a stage that needs files of its own. A sentinel file inside names
the stage’s type; STAGE.md is the sequential one, and carries the frontmatter
and body that 01-name.md would have carried. Other sentinels name other types,
listed under stage types. A schema, a gate, and hooks all live in
the folder, so a stage that needs any of them takes the folder form.
In a sequence, the leading number declares order: stages run from the lowest
number to the highest. A number is one or more digits followed by a hyphen, and
it is compared as a number, so 9- runs before 10- and leading zeros change
nothing. Gaps are fine. Two entries whose numbers are equal is an error. The name
after the number is the stage’s identity. Some containers hold stages that do
not run in sequence — the branches of a fan-out, the alternatives of a choice —
and those are named without a number; the control graph says which.
Nothing invokes a stage by name. A stage runs because the flow reached it.
What a stage folder holds
Section titled “What a stage folder holds”Beside its sentinel, a stage folder may hold any of the following, and all of them are optional.
| File | Purpose |
|---|---|
schema.json, schema.md |
the output’s contract and format |
gate, or a gate/ folder |
checks the output must pass |
before |
runs before the agent loop |
success |
runs after the output passes |
failure |
runs after the stage fails |
skills/ |
skills only this stage can see |
subflows/ |
flows only this stage can call |
The runtime executes the three hooks (before, success, and failure) and
the gate; the operating system decides how to run them. Each needs its
executable bit set and a shebang line, or is a compiled binary, so any language
will do. A script may carry an extension naming its language — gate.sh,
gate.py, gate.rb, gate.ts — or none at all. The extension is for whoever
reads the folder; the runtime ignores it.
A folder holds one file of each kind. Two files whose name before the first dot
is before is an error, as is more than one schema, as is a gate file beside
a gate/ folder. A schema’s extension is the exception to the rule above: it is
not decorative, and it chooses the output’s format.
The prompt and the configuration
Section titled “The prompt and the configuration”The markdown body is the prompt — the instruction the agent receives. The
frontmatter is the configuration, and it is closed: an unknown key is an error
naming the key. A stage whose body is empty is refused (body-missing) — a
stage with no instruction asks nothing, and an agent with no instruction is a
mistake, not a stage. A body is decoded as UTF-8, and bytes that are not
UTF-8 become replacement characters — deliberately: the body is prompt text,
the assembly can honestly run, and the author’s garbage reaches the agent
visibly rather than refusing a runnable assembly.
intelligence: defaulttimeout: 3600retries: 2workdir: ./investigatorEvery inherited key falls back to the assembly’s when a stage does not set it.
intelligence selects one complete bundle defined by
the home. timeout is the agent loop’s budget in seconds,
3600 by default, and it covers every round of the agent’s work rather than each
one separately — a held agent is still on the clock it started with, and only
the agent’s own time spends it. Checks and
hooks run on clocks of their own and never spend the agent’s
(invariant 22). An agent that crosses it is stopped, and the
stage fails. retries bounds how many times a failing check may send the
agent back, and defaults to 2. A refusal or reported fault skips checks and
does not spend a retry (control tools).
None of these reaches the agent. Which model is running, how long it has, and how many retries remain are the runtime’s business — an agent told its budget paces itself against the budget instead of the work.
local-context retains its current meaning, deciding for one stage what
$PWD’s own context does
(invocation). The stage-only
workdir also retains its meaning below. Neither changes the selected
intelligence bundle.
workdir is different from those inherited options. It belongs only to a
STAGE, and names that stage’s working directory relative to the root workspace
selected by bot run start --in. It is an explicit path, not a name inferred from
the stage folder. Absolute paths, .. paths, and . are refused
(value-invalid), and a path that is not already a directory is refused
(path-missing) before provider work starts. Omitting it keeps the flow’s
current working directory, which is the root workspace for a top-level flow and
the calling stage’s $PWD for a subflow.
The workspace is caller-owned. BotAssembly does not create, empty, retain, or
remove a stage’s workdir; it only starts that stage’s agent, hooks, and gates
there. Runtime-owned input, output, skills, and scratch keep their existing
lifetimes outside it (slots).
These rules do not change inside containers. In a PARALLEL, each branch stage
resolves its own authored workdir; branches that author the same directory, or
omit the field, share it. Concurrent writes to a shared directory are the
author’s responsibility. Every repeat of a LOOP stage uses the same authored
directory, so files left by an earlier repeat remain visible to later repeats;
its scratch and session identity remain repeat-specific. A subflow called from a
stage inside a container inherits that stage’s resolved directory, just as a
linear subflow does.
The inherited option keys are legal in every sentinel, and a container’s apply
to everything inside it (the control graph).
Each container adds its own — repeat on a LOOP.md, width on a
PARALLEL.md — and nothing else is accepted there. workdir is the stage-only
exception.
Frontmatter carries only what placement cannot say. A stage’s position, its order, and the folder that holds it are read from the filesystem, never restated as keys.
Input and output
Section titled “Input and output”The agent reaches everything through slots — environment variables naming what it needs, never paths it had to find.
$INPUT is a directory holding one file per source. A stage in a sequence
has one source, so its $INPUT holds one file, named after the stage that wrote
it: after 02-design.md, $INPUT/design.txt. A stage following a fan-out has
several, each named after the branch that produced it. Nothing is ever merged;
the filenames are the namespace.
$OUTPUT is one file. The stage’s output is what the agent writes there and
nothing else. The schema chooses the format, and therefore the extension. A
stage with no schema writes text — whatever the agent produced, as it stands —
and its output is named .txt.
A schema is a file, so a stage that wants one takes the folder form. A stage
written as a single 01-name.md always produces .txt.
$OUTPUT does not sit under $PWD. The caller-owned tree may be shared — other
runs and other branches may be in it at the same time — so the one file that
has to survive the stage lives somewhere the runtime owns.
The runtime names the input files in the prompt and states what the schema requires. How those facts are worded is prompt construction.
The agent loop
Section titled “The agent loop”A stage’s work is an agent loop: the model responds, calls a tool, reads the result, calls another, and carries on until it stops calling tools. One stage is one agent loop, running in one session — the log of everything the model has said and done. The session starts fresh at every stage. What reaches a stage from earlier stages is their outputs, never their sessions.
The loop ends when the agent stops and the stage’s checks let it go. A failing check does not start the agent over: the agent has not left, its session is intact, and the reason it was held goes into that session as one more thing it has read.
An agent that cannot do the job says so instead. It calls the refusal tool with a reason, and the stage ends there: no checks run, because there is nothing to check, and no retries are spent, because the agent is not asking for another try. The stage fails, the reason is recorded, and the flow stops. A refusal is a result: it says in the agent’s own words what stopped the work, which is the most useful thing a failed stage can leave behind.
Gating
Section titled “Gating”Three checks stand between the agent stopping and the stage ending, and each is optional (gating).
| Order | Check | Judges |
|---|---|---|
| 1 | the checklist | what the agent did |
| 2 | the schema | the shape of the output |
| 3 | the gate | whether the work is good |
An agent that stopped without writing $OUTPUT is caught before any of them.
The first ordinary failure ends the round and sends the agent back into its own
session with the reason. retries bounds how many send-backs a stage allows,
whichever check caused them: at the default of 2, three rounds at most, and the
stage fails when the last one does. A gate exiting 75 with nonempty captured
output is an external blocker instead: it ends the stage immediately with exit
1, cause blocked, and its output as the reason, without a send-back or
retry.
Three hooks bracket the agent loop (hooks). Each runs with the same slots in its environment as the agent, and changes things by writing files.
| Hook | Runs | May change |
|---|---|---|
before |
before the agent loop | $INPUT |
success |
after the output passes every check | nothing |
failure |
after the stage fails | nothing |
So before rewrites what the agent sees; success reads and publishes what
passed — the output is no longer anyone’s to change (hooks).
A hook’s own exit code counts. before exiting non-zero fails the stage before
the agent has run at all, and the record names before as what failed.
success exiting non-zero fails a stage whose output had already passed its
checks. failure runs whenever a stage fails, including that first case, where
there is no output for it to look at; it runs after the fact. Its exit, timeout,
inability to execute, output overflow, or hash-recheck drift is diagnostic
evidence only and cannot replace the stage’s existing ending.
A hook runs once per stage. A held-back agent is still inside the same stage, so
being sent back by a check does not re-run before.
Inside a stage
Section titled “Inside a stage”flowchart TD input([input]) --> before[before] before --> agent[agent loop] agent -- refuses --> failure[failure] agent -- stops --> checklist{checklist} checklist -- unfinished --> retries{retries left} checklist -- accounted for --> schema{schema} schema -- invalid --> retries schema -- valid --> gate{gate} gate -- 75 with output --> failure gate -- other non-zero --> retries gate -- zero --> success[success] retries -- yes --> agent retries -- no --> failure success --> output([output]) failure --> stop([flow stops])beforeruns. What it writes is the agent loop’s input.- The agent works until it stops, or until it refuses.
- The checklist, the schema, and the gate run in that order. The first
ordinary failure ends the round; a gate exit
75with nonempty output is an external blocker and fails the stage immediately. - An ordinary failure puts its reason into the agent’s session and sends the
agent back to 2, without re-running
before. When the retries run out, the stage fails. - A refusal skips 3 and 4 entirely and fails the stage.
successorfailureruns, according to the outcome.
Any step the stage did not declare is skipped.
A stage exits with one of these codes.
| Code | Meaning |
|---|---|
0 |
success |
1 |
failure — a check said no, the agent refused or ran out its clock, a before/success hook ran cleanly and exited non-zero, or a gate reported an external blocker: the stage working as designed, the work rejected or blocked |
2 |
the run was found impossible while the stage ran — a gate or before/success hook that could not execute, hung, or broke a passed output, or the machinery beneath the run failing; failure-hook breakage is recorded diagnostic evidence and keeps the prior failed ending |
128+n |
killed by signal n: 130 is Ctrl-C, 143 is terminated |
A stage malformed on disk never reaches this table. The whole assembly is read
and checked before anything runs, so a stage that would not have parsed simply
never runs, and the run itself exits 2. The 2 above is the same fault found
late — the folder was well formed and one of its executables was not
(the runtime).
A stage that exits non-zero stops the flow: later stages do not run, and the flow exits with that same code.
Stage types
Section titled “Stage types”A stage folder holds exactly one sentinel file, and the sentinel names the stage’s type. Zero sentinels, or two, is an error. All sentinel files are capitalized.
| Sentinel | Type |
|---|---|
STAGE.md |
runs once, in its numbered position |
LOOP.md |
repeats its contents |
CHOOSE.md |
selects one of several alternatives and runs it |
PARALLEL.md |
runs several branches at once |
FANOUT.md |
runs one subflow per checked list item (fan-out, provisional) |
DESCEND.md types a flow rather than a stage, so it never stands in a stage
folder (descend). Every other sentinel is in the table.
Stage in this document means the first row, in either form: a folder typed
by STAGE.md, or a single 01-name.md. Those run an agent and produce an
output, and everything above this section is about them.
The rest are containers. A container arranges stages and passes along what they produced. It has no hooks and no checks, because it produces no output of its own to check (the control graph).
What each container decides, and how, is the control graph.
The flow
Section titled “The flow”Stability: stable.
A flow is a procedure: do this, then this, then this. It is a folder under the
assembly’s flows/ directory, typed by a FLOW.md file, holding numbered
stages.
flows/ review/ FLOW.md 01-read.md 02-analyze/ STAGE.md schema.json gate.sh 03-report.mdStages run from the lowest number to the highest. Nothing else decides the
order, and nothing inside a stage names what comes next
(the control graph). A sequence number is at most nine digits —
enough for any flow, and short enough that every reader orders them the same
way (number-invalid).
How the stages connect
Section titled “How the stages connect”Each stage’s output becomes the next stage’s input. In slot terms, what
02-analyze wrote to $OUTPUT is what 03-report finds in $INPUT, under the
name analyze (slots).
The first stage’s $INPUT holds the run’s request. The flow’s output is the
output of its last stage.
The flow’s exit code is the exit code of the first stage that failed, or zero if none did. A stage that fails stops the flow: the stages after it do not run.
FLOW.md
Section titled “FLOW.md”FLOW.md is frontmatter followed by an optional procedure body. The body
explains work shared by every stage in the flow, and tells each fresh context
its enclosing structure and static root step. A missing or whitespace-only body
is silent — it adds nothing to a prompt. The fences are still written, because a sentinel is
frontmatter and body everywhere (invariant 42):
---description: Review a change and report what is wrong with ittmp: flow---
Read the change, verify each finding, then write the report.description is required and is one line. It is what a routing decision reads,
so it should say what the flow is for rather than how it works; it is not prompt
content. The procedure describes the whole flow, not the instruction for one
stage.
tmp is stage or flow, and it says whether every stage gets its own scratch
directory or they share one for the whole flow (slots). It is
stage unless this says otherwise.
Any of the assembly’s authorable defaults — timeout, retries,
local-context, and intelligence — may be set here, and every stage in the
flow inherits them unless it sets its own
(the assembly).
Routing
Section titled “Routing”A run that names a flow runs that flow. A run that names none runs the assembly itself: one agent with every flow and every root subflow in scope, calling whichever fit the request and answering with what comes back (running the assembly).
Skills
Section titled “Skills”A skills/ directory in the flow folder is visible to every stage in that flow
and to no stage outside it. This is the middle of the five skill scopes
(skills).
Subflows
Section titled “Subflows”A subflows/ directory in the flow folder holds flows that every stage in this
flow can call, and no stage outside it. This is the middle of the three subflow
scopes (subflows).
Where the checks live
Section titled “Where the checks live”Hooks, schemas, gates, and checklists belong to stages (gating). A flow that wants its input normalized or its result published says so in the stage at that end of the sequence, which is where the work is.
The assembly
Section titled “The assembly”Stability: stable.
An assembly is a folder holding ASSEMBLY.md, a flows/ directory, and
optionally skills/ and subflows/.
review-bot/ ASSEMBLY.md flows/ review/ FLOW.md 01-read.md skills/ house-style/ SKILL.mdAn assembly holds ASSEMBLY.md and flows/; without either it is not one. By
default an assembly holds no other visible root entry. It may declare opaque
top-level folders as described below. An ASSEMBLY.md anywhere in the grammar’s
tree below the root refuses at check. It is referenced by name — resolved in
the home — or by an explicit path, and no other way. There is no search of the current directory, no
layering, no shadowing, and no consent step: naming a folder is the act of
choosing to run it.
ASSEMBLY.md
Section titled “ASSEMBLY.md”The frontmatter carries the defaults every stage inherits. An assembly’s name
is its folder’s path under the home’s assemblies/ — like every name in this
format it comes from the filesystem, and nothing inside the folder restates it
(invocation).
intelligence: defaulttimeout: 3600retries: 2| Key | Rule |
|---|---|
intelligence |
optional; names one complete choice the home defines |
timeout |
3600 seconds |
retries |
2 |
local-context |
one of ignore, announce, use; ignore |
slots |
optional; names and describes the slots this assembly declares |
tmp-max-bytes |
optional positive safe integer; the live $TMP ceiling, one GiB (1024 ** 3) by default |
strict |
optional boolean; whether unknown visible root entries refuse; true by default |
folders |
optional YAML list of opaque top-level folder names |
Each folders item is a nonempty, non-absolute single path component. It is
not ., .., and contains neither / nor NUL. The reserved names
ASSEMBLY.md, flows, skills, subflows, README.md, LICENSE, and gate
cannot be declared. A declaration need not exist; when it does, its root entry
must be a real directory, not a file or symbolic link. Duplicate declarations
have no additional effect.
A declared folder is opaque. Bot establishes only that its root is a real directory, then does not enumerate, validate, execute, or capture its contents. The declaration applies to that one root component, not recursively within folders the grammar owns. Its contents do not contribute to procedure identity. Installation and update may still carry the folder as transport data.
A copied installation omits every source entry whose basename starts with .. Bot writes its own root .bot-source after the copy. A linked assembly keeps its source tree unchanged.
With strict: true, any other visible unknown root entry is refused as before.
With strict: false, any other visible unknown root is likewise opaque and
excluded from procedure identity. Grammar-owned roots are always read and
validated. Hidden entries keep their existing treatment. Invalid policy fields
grant no omission, preserving strict validation.
An authored rung selects at most one intelligence name. If the assembly does not select one, model choice follows the existing precedence rules; every flow and stage that does not override the assembly inherits its selection (invocation). Other unset assembly options fall back to the home or built-in defaults by those same rules.
The body is the assembly’s purpose statement, and it enters the context of every stage (prompt construction).
HTML comments are author-to-human and are stripped from every body before it
reaches an agent — here, in a stage, in a LOOP.md, in a CHOOSE.md. Files on
disk are never rewritten. A body that is nothing but comments is an empty body.
Skills
Section titled “Skills”A skills/ directory at the assembly root is visible to every flow and every
stage. This is the widest of the five skill scopes, and the one to use least
(skills).
Subflows
Section titled “Subflows”A subflows/ directory at the assembly root holds flows every stage can call.
This is the widest of the three subflow scopes, and the same advice applies:
a subflow belongs at the narrowest scope where it is useful
(subflows).
Validation
Section titled “Validation”The manifest is read before descendants so its top-level traversal policy can
be applied. Every non-opaque entry is either understood or refused, and a
refusal names the file, directory, or key at fault. A malformed assembly exits 2 and writes no record
(the runtime).
The home
Section titled “The home”Stability: stable.
The home is the directory a runtime keeps everything durable in; scratch lives in a cache the runtime owns (slots).
$BOT_HOME/ installation.json config.yaml assemblies/ review/ ASSEMBLY.md flows/ runs/ 2026-07-30T14-22-08-a3f9/$BOT_HOME if it is set, otherwise ${XDG_DATA_HOME:-~/.local/share}/bot.
A --home given on the command line names the home directly and wins over
both.
A first bot run start creates the home’s installation identity before run birth when the record is absent. A first bot run resume does the same after it accepts the donor and before it creates the new run. Concurrent first runs use the one identity that wins atomic publication. A run leaves an existing valid identity unchanged. An invalid record refuses the run without repair before provider contact, run-directory creation, run_start, or id-file publication.
bot home show --home DIR reads the installation identity without mutation. It requires the explicit option and ignores BOT_HOME and the platform default. Markdown is the default. --json and -j return a newline-terminated version-1 bot.home.show document. The data.paths object reports config, runs, assemblies, installation, cache, and piAuth. Each entry carries the absolute path Bot resolved and whether it exists at the time of the reading. The Pi session directory is not reported because Bot does not resolve it. Markdown reports the same six paths and standings after Initialized. Each result occupies at most 65,536 UTF-8 bytes. An absent show succeeds with initialized: false and creates nothing.
installation.json is an owner-only (0600) regular file in an owner-only (0700) real home directory. It contains one canonical newline-terminated version-1 bot.installation object of at most 1,024 bytes with one lowercase UUID version 4 identity. Bot refuses malformed, linked, oversized, non-private, wrong-owner, changed, or unsupported records without repair. Bot resolves the real parent and uses canonical pathnames for safe filesystem operations. It opens final objects without following links, compares pathname and descriptor identities, and revalidates the parent and home before and after reads and mutations. A missing home or record produces the absent reading only after the containing objects still validate. Bot detects pathname replacement that these checks observe. The local same-Unix-account trust model excludes an actor that races replacement of an intermediate ancestor. Initialization synchronizes the held parent before it publishes a complete candidate through create-if-absent linking. It then removes its exact private temporary name and synchronizes the held home. Concurrent initializers return the published winner. A non-EEXIST link rejection is retryable when finalization succeeds and never claims possible publication. Cleanup, synchronization, and close failures return an integrity failure. They retain the publication-may-have-completed diagnostic only after a successful link. Moving a valid home preserves the identity. A copy preserves it only when bytes, ownership, and modes remain intact. Bot provides no reset command.
That is the whole of the resolution, and the omissions in it are deliberate.
Runs are state and config.yaml is configuration, so a strict reading of the
XDG directory specification would put them under $XDG_STATE_HOME and
$XDG_CONFIG_HOME; they live in the home instead. The home is ONE folder
holding everything bot owns — what a person backs up, copies to another
machine, points a second bot at, or deletes to be rid of it — and three
directories that must be kept in step are three ways for a home to be half
there. --home names all of it or none of it, which is what makes a throwaway
home a single argument. Three things are NOT in it. Scratch is out for a reason
of its own: it is a cache, and no slot value may disclose where runs live
(slots). Credentials are out for the opposite reason to everything
that is in — a login belongs to the operator, where a home is a workspace, and
repeating a login per project is not a thing a person should have to do. They
live in auth.json under Pi’s resolved agent directory, whose default is
~/.pi/agent/auth.json, with the directory at mode 0700 and the file at mode 0600; --home does not move them and no
bot-named variable points at them (authentication).
What is in it
Section titled “What is in it”assemblies/ holds assemblies by name. A name is a relative path under
this directory — review, or team/review — and that is the whole of the
registry: no index file to keep in step, no duplicate-name validation, nothing
to drift (the assembly,
invocation).
An entry standing at an assembly’s name under assemblies/ may be a symbolic
link to a directory; a nested name makes its parent directories real, and the
link stands at the leaf.
The link is the editable install — deleting it is the uninstall, and the tree
it points at is never touched — and everything inside the resolved assembly
keeps the symlink ban (the graph). What ran is still answerable,
because the record hashes the bytes it read, not the path it read them through
(the record). The commands that make and unmake these entries are
management.
runs/ holds one directory per run — its record and its sessions
(the record, the session). This is data with a
retention policy, where an assembly is source somebody wrote. A run in flight
also has a lock beside its directory, named for it; the lock is the runtime’s
rather than the run’s, and it leaves when the run does
(the runtime).
config.yaml holds loose non-model defaults and the named model choices a
run can use.
timeout: 3600retries: 2local-context: ignoreintelligences: default: provider: anthropic model: claude-opus-5 reasoning: medium coder-hard: provider: anthropic model: claude-fable-5 reasoning: xhighIt is a strict plain YAML file. Its loose keys are timeout, retries, and
local-context; model choice is present only in intelligences. Retired loose
model-choice keys and unknown keys are refused (key-unknown).
Intelligences
Section titled “Intelligences”intelligences is a flat table of complete model choices by name. Every row
requires model and reasoning (key-missing); provider is optional, and
any other bundle key is refused (key-unknown). Nothing in a bundle is a
credential.
The name is an option at the command, task, stage, container, flow, or assembly
rung. The nearest name supplies provider, model, and reasoning as one bundle.
When none is named, an executing agent looks up the reserved name default.
That implicit name and its bundle are stamped home; an explicitly authored
intelligence: default keeps its authored rung. A missing row is refused as
intelligence-unresolved only when an agent needs it.
A providerless bundle uses normal model lookup. If more than one provider in
the catalog offers the model, the run is refused as model-unresolved naming
the candidates. If the catalog holds no model of that name, the run is refused
as model-unresolved naming the catalog it read and nothing beyond it. If a
provider holds the model and the runtime can see no credential for that
provider, the run is refused as credential-missing before it is born, naming
the provider to sign in to (refusals). Every one of
these refusals names the model string as authored, the rung it resolved from,
what the runtime observed, and one command.
A run snapshots the table at start and records the resolved name and complete bundle without exposing the name in an agent prompt.
The home and each run directory are created owner-only (0700), like ~/.ssh.
Nothing inside is made private separately — the directories close the tree — and
the contents of a home that already exists are left as they stand. Its own mode
is not: a run refuses a pre-existing home that is not a real directory owned by
the effective user at exactly 0700, before run birth. The scratch tree is created the
same way, at its root: it holds the same prompts, inputs and outputs the run
directory holds, so it is closed by the same door (slots).
A home that does not exist is an empty rung, not an error. Nothing resolves from it, and nothing complains until a run needs what only it could have supplied.
What inherits from it
Section titled “What inherits from it”The home is the bottom rung. Every value resolves from the most specific place that sets it (invocation):
command line → task file → stage → container → flow → assembly → home → built-in defaultThe task-file rung is defined in Invocation.
An assembly names an intelligence, never a literal model. The home maps that
portable name to local machinery. An omitted name uses the reserved default
row only when an agent needs a model choice.
The agent never learns it exists
Section titled “The agent never learns it exists”No slot points into the home. The agent is not told where assemblies live, where
its own assembly lives, or that other assemblies exist at all
(invariants). A stage’s input, output, scratch, and skills are
named for it, and the tree it works in is somewhere else entirely
(slots). A caller’s $BOT_HOME is the runtime’s to read and never
the agent’s to see: it is the runtime’s own bot-named variable scrubbed from
every stage’s environment. Provider credential environment names consumed by
the parent are scrubbed too, and a stage’s shell holds that environment whole —
what the runtime composed, with nothing of the runtime’s own process beneath it.
A ~ at the front of a path an agent hands a file tool is refused for the same
reason: it is shorthand for a directory the agent was never told about. That is
obscurity and not prevention, like everything else here
(invariants) — a path written out in full still reaches
wherever the operating system allows.