AGENTS.md@packages/prompt · diff
git:20261002.71c64c0 to git:20261002.55f3cda
6 added, 2 removed. Audit A to A.
# prompt
## Code-mode guidance
- The shipped `loom-default-14` pack asks for immediate code mode on planned
+ The shipped `loom-default-15` pack asks for immediate code mode on planned
batches and chains whose intermediate results need no agent judgment. The
third extraction probe remains the fallback for investigations that grew into
a batch. Bounded `cap/task.parallel_map` guidance is conditional on the selected
seam, and completeness, truncation, `cap://` discovery and warning-free
compilation remain explicit. Stateful external mutations retain an agent
- judgment step between calls.
+ judgment step between calls. Tested reusable programs belong in real workspace
+ files with purpose and input notes. `program_path` reuses that source under
+ current permissions; every new invocation reloads it and collects fresh LSP
+ facts. SQL observations remain local to one invocation, and saved source carries
+ no retained authority.
## Purpose
Model-facing prose as swappable data. A **pack** is a file of named,
ordered sections carrying `{placeholder}` holes; this package decodes one
with a total decoder and renders it. Prompt *words* belong in a pack,
never in Gleam source — that is what lets them be mutated, scored and
replaced without a recompile. Reading a pack file, populating the
environment, and pinning a rendered string are all outside this package:
it performs no I/O.
One pack ships: the **system pack** (`prompt/default.source`), rendered
through `pack.render` into the one string a session sends as `system` on
every request of every strand, behind a one-hour cache breakpoint. A
second, summarization pack used to ship beside it and was removed with
the summarizer it fed — compaction now publishes the model's own notes
(`client/checkpoint`) and asks no provider to condense anything — so this
package holds no prompt that is read once per compaction, and the
`pack.section`/`pack.fill` doors it assembled through are kept as the
general reading API they always were.
## Key Types
- `prompt/pack.Pack` — a decoded pack: `version` (the pack's own
identity, recorded so a cache miss can be attributed to a prompt
change), `digest` (an FNV-1a fingerprint of the source it came from,
for change detection only — not an integrity check), and `sections` in
file order.
- `prompt/pack.Section` — `name` plus `template`. A name beginning with
`_` marks a **fragment**: never rendered in its own right, only
reached through a placeholder whose value the environment selects.
That is how the sandbox section says one thing on a fully enforced
host and another on a degraded one without the alternative wordings
living in Gleam.
- `prompt/pack.Environment` — opaque; built only through
`pack.environment(...)`, which trims, sorts and de-duplicates every
list field. Workspace, platform, shell, tools, available tools,
enforcement, network posture, protected paths, repository guidance.
Nothing else, and nothing numeric. `available_tools` — the one-line
snippets that become the prompt's tool index — is the single field
trimmed and de-duplicated but **not** sorted: its order is the host's
registration order, which is fixed for the session and is what a reader
wants, so sorting would scatter a host's own tools through the
built-ins and hide an unstable registry rather than fix one.
- `prompt/pack.Enforcement` — `FullyEnforced` / `PlatformEnforced` /
`DegradedRefusing` / `BestEffort`. Behavioural posture, not a layer inventory; see
Invariants.
- `prompt/pack.NetworkPosture` — `NetworkBlocked` /
`NetworkProxied(allow)` / `NetworkOpen`, mirrored from
`broker/policy.NetworkPolicy` because a pure package cannot depend on
the broker. The proxy address is deliberately not carried.
- `prompt/pack.decode` — `String -> Result(Pack, CorruptionReport)`, the
total decoder. `pack.encode` is its inverse for an optimizer writing a
mutated pack back out.
- `prompt/pack.render` — `Pack, Environment -> String`. The whole point
of the package, and a pure function of exactly those two values.
- `prompt/pack.problems` — `Pack -> List(Problem)`, reporting missing
canonical sections, missing fragments, and placeholders no binding
provides. Deliberately separate from `decode`: a mutated pack that
drops a section is still a valid pack, and the harness decides whether
to run with it.
- `prompt/pack.Severity` — `Corrupting` / `Shaping`, and
`prompt/pack.severity : Problem -> Severity`. **Corrupting** is the
pack naming something it does not carry: a placeholder no binding
provides, or a fragment a binding selects. **Shaping** is the pack
being smaller than canonical: a section is absent. The line is drawn
where intent is: a typo renders empty and says so nowhere, a dropped
section may be exactly what a mutation meant.
- `prompt/pack.Assessment` / `prompt/pack.assess` — `problems`
partitioned by `severity`, so `assess(pack).corrupting == []` is the
question a prompt optimizer asks (is this variant scorable at all?) in
one expression. It is a partition and nothing more: between them the
two lists hold exactly what `problems` returns.
- `prompt/pack.section` / `prompt/pack.fill` — reading one named
template out of a pack, and filling its holes from an association
list under exactly `render`'s substitution rules. `fill` is the *only*
substitution in the package, which is what keeps the never-re-scanned
property in one place; nothing in the tree assembles through these
doors today, since the summarization pack that did is gone.
- `prompt/default.source` — the system pack Loom ships with, as pack
source. There is no helper returning an unwrapped `Pack`; producing
one would need a crash-ladder construct, and the harness has to handle
a failed decode for an operator-supplied pack anyway.
## The pack format
`%%` in column zero makes a line a directive; every other line is body
text of the open section, kept verbatim with no escaping of any kind.
Three directives exist: `%% loom-prompt-pack <n>` (format version,
required, first), `%% version <id>` (the pack's identity, required,
once, before the first section), `%% section <name>` (`[a-z0-9_]+`).
A directive whose content begins with `#` is a comment; any other
unrecognized directive is corruption. The cost of that strictness is
that a body line may not begin with `%%`.
The default pack's identity section explains notes-based checkpoints,
small stable note keys, exact history reads, and the absence of a guaranteed
final note-writing turn. It asks agents to maintain their own inherited
requirements and verify recalled facts. Mutable notes remain user-context
data. Existing pinned prompts are not migrated; this rollout starts fresh
sessions.
The `conduct` section carries one short sentence pointing multi-step work at
the `todo` tool when it is offered. The tool's own description carries the
mechanics and the three-step threshold, and the sentence is kept short because
the rendered prompt has a byte budget (`system_prompt_test`).
The default system pack carries the canonical sections — `identity`,
`tool_discipline`, `available_tools`, `delegation`, `conduct`,
`environment`, `sandbox`, `repository_guidance` — plus the fragments
three of them select between. `available_tools` renders the host's tool
index through the `_available_tools` fragment and disappears entirely on
a host whose registry offered no snippets.
`pack.canonical_sections` is the list, in render order, and the shipped
pack is held against it.
## Relationships
- **Depends on**: `core` (`core/corruption.CorruptionReport`, the one
error type every total decoder returns) and `gleam_stdlib`. Nothing
else, ever — see Invariants.
- **Depended on by**: `client`, through `client/system_prompt` (reads
the pack file, builds the `Environment` from the workspace, the
helper's hello and the composed sandbox policy, renders once at
session open, and pins the result into the reserved `prompt/` cell).
- **FFI**: none, and there must not be any. There is no
`internal/ffi_*` module here, no `@external` of any target, and no
`gleam_erlang` or `gleam_otp` in `gleam.toml` — by rule, not by
coincidence.
Two properties rest on that, and one `@external` closes both: purity is
what makes the state space property-testable without spawning processes,
and the same discipline is what keeps this package compiling to the
**JavaScript target**. Lint R6 gates on it at error level and its census
must stay zero. Portable here means *decide but not act* — replay a
conversation tree, validate a transcript with the server's own total
decoders, run `next_action` over fetched state — and never the harness in
a browser: `gleam_otp` has no JavaScript target, Rule Zero is
kernel-enforced (in a browser the harness VM and the untrusted-code VM
would be the same VM), and the two-channel doctrine needs processes on
both sides. `docs/gleam-style.md` Part IV §5 argues it in full.
## Traffic
None. `prompt` spawns nothing, commits nothing, reads no register, and
crosses no wire. It is a pure decode-and-render library; the durable
`prompt/` blackboard cell the design pins the rendered string into is
written by whoever calls `render`, not here.
## Invariants
- **`render` is byte-stable for a session.** It is a pure function of
`Pack` and `Environment`; every `Environment` field is fixed at session
open. No clock, date, elapsed time, token count, cost, git state,
operation id, entry id, strand name or random value can appear in the
output, because none can enter the inputs. This is what makes the
system prompt cacheable behind a one-hour breakpoint; a single changed
byte costs a full cache write at 2× base input on every strand for the
rest of the session.
- **The purity is structural, not a convention.** The dependency set is
`core` + `gleam_stdlib`, so no time source, id generator, process or
filesystem exists in this package's graph to reach for.
`test/prompt/purity_test` reads `src/` and fails if an import, an
`@external`, or a numeric `Environment` field appears.
- **`Environment` has no numeric field and must never grow one.** A
timestamp, an elapsed count, a cost and a token total all arrive as an
`Int`. Adding one is how the caching contract gets broken silently.
- **Every list field is normalized at construction** — trimmed, emptied,
sorted, de-duplicated — so a caller's discovery order cannot reach the
rendered bytes. The tool array on the wire must be sorted for the same
reason; it sits *ahead* of the system block in the cached prefix.
- **Decoding is total.** A missing header, an unknown directive, a
duplicate section name, a bad name, a wrong format version: each is a
`CorruptionReport` naming the line. Duplicate section names are
corruption for the same reason duplicate keys are in `core`'s
codecs — two sections of one name have no single meaning.
- **Rendering is total and never re-scans.** An unknown placeholder
renders empty, an unclosed or non-identifier brace renders literally, a
missing section is absent, and a section that renders to nothing is
dropped with the blank line that would have followed it. A substituted
value goes straight to the output and is never scanned again, so
injected repository guidance containing `{shell}` renders those
characters and no pack can drive expansion in a loop. `pack.fill`
carries the identical property, and it is the property any future
caller that splices model output or tool results into a template
depends on: a second pass would let that content name a binding and
expand it.
- **The prompt tells the model its window is finite, once, in
`identity`.** Compaction replaces the older part of a context with the
model's own notes (`client/checkpoint`), so the pack says so up front
and asks for notes as the work goes — requirements, decisions, failed
approaches, test results, exact identifiers — rather than when asked.
That is the one place the compaction design reaches the prompt; the
words at the boundary itself are the checkpoint's, rendered by
`client/checkpoint` at compaction time and never cached.
- **The sandbox section states posture behaviourally, never a layer
inventory.** Naming which kernel layers a host does or does not
enforce hands an injection payload a map of the holes for something it
could read as ground truth from one shell command anyway, and tells a
cooperative agent nothing it can act on.
- **The enforcement line is present on every host.** Its presence
everywhere is what makes its variation meaningful.
- **A degraded host's sentence names a host failure, not a policy
denial.** Under `FullEnforcement` and the production
`PlatformEnforcement` default, a degraded helper means every jailed execution is refused, before
dispatch and again after the run. Escalation cannot clear it and
retrying cannot either, and an agent that mistakes it for a policy
denial retries forever against a wall. The two demand different
behaviour, so the prompt distinguishes them.
- **`Enforcement` carries only what the harness can know at session
open**: the demanded posture plus the coarse `degraded` flag from the
helper's hello (`broker/exec.degraded_features`). There is no
per-layer report at open — the `skip:` list lives inside an
`ExecResult`, after a run, and the `ENFORCED`/`SKIPPED` table is a
separate `--self-test` invocation. Fields with no source do not exist
here.
- **Repository guidance is project-authored data, framed as such, and
carried whole.** There is no byte budget in the render: pi and
oh-my-pi carry these files whole too, and a cut the operator did not ask
for costs more in instructions silently unread than it saves. The one
bound is upstream, in `client/system_prompt.max_guidance_file_bytes`,
where a file over a megabyte is not read at all. Framing does not make a
hostile `AGENTS.md` or `CLAUDE.md` safe; it stops one speaking with the
operator's voice, and the residual risk is accepted and named.
- **The `repository_guidance` binding may carry more than one file, and
the fences around them are `client/system_prompt`'s.** This package
still sees one opaque string and still never parses it. What
`_repository_guidance` promises about that string is the contract the
client keeps: each file arrives inside an `<instructions>` block naming
its path and an origin of `workspace` or `user-default`, at most one
block is `user-default`, and when there is one it is first. Reword the
fragment and those two words move with it — they are the only thing
telling a model which instructions are its operator's.
- **`decode` accepts more than `problems` approves.** Keep it that way:
syntax is the decoder's business, completeness is the harness's
decision. `severity` refines the *report* and must never reach back
into the parser: a pack `assess` calls corrupting still decodes and
still renders.
- **A missing section is `Shaping`, never `Corrupting`.** A mutated pack
that drops a section is a valid pack; the severity axis exists to let
an optimizer keep scoring one, so nothing may reclassify it into a
refusal.
- **The delegation section says only what an `agent_*` schema cannot.**
The six schemas are on the wire already and the pack does not repeat
them. What it carries is the policy: a wait holds the operation open
and queues a human's steer, so batch the spawns and wait on the batch;
addressing is parent-or-descendant and a wait is descendant-only,
which is what keeps the wait graph acyclic; a child's result is its
last assistant message plus its blackboard notes, not a structured
report, so a brief must ask for a self-contained final answer. Each
sentence is checked against `tools/agent` and `client/agency` by a
test in `default_test`; if one of those changes, the sentence is
wrong, not merely stale.
## Deep Docs
- [docs/architecture/prompt.md](../../docs/architecture/prompt.md)
— how a request's prompt is assembled from packs, project instructions,
the tool array and skills, and what must stay stable for caching.
- [docs/design-notes/agent-comms-and-system-prompt.md](../../docs/design-notes/agent-comms-and-system-prompt.md)
— Part B: the design this package implements, including the stability
contract and the alternatives rejected. Part A is the `agent_*` tool
semantics the `delegation` section states policy for.
- [docs/review/m5-agent-comms-judgment.md](../../docs/review/m5-agent-comms-judgment.md)
— claim 4 and change item 5, which overrode the design's sandbox
reasoning and are what the wording here follows.
- [docs/architecture/compaction.md](../../docs/architecture/compaction.md)
— the checkpoint that replaced the summarization pack, and why.
- [docs/architecture/orchestration.md](../../docs/architecture/orchestration.md)
— the plane the rendered prompt is consumed in.
- [Root CLAUDE.md](../../CLAUDE.md) — repo ground rules and the doc
graph.
## Durable code-mode notes
The context guidance directs programs to cap/notes only when code_mode
advertises it. Structured data can stay in the durable session blackboard
while the program returns a small summary. It distinguishes this from evictable
cap/kv scratch and makes virtual note reads conditional on the advertised API.
See [protocol 045](../../protocol-change/045-code-mode-notes.md).
## Code-mode utilities
The context guidance points agents to the advertised executable recipes and
states that the default code-mode program can combine workspace effects with
agent orchestration. It names workspace as the default mode and directs the
model to the tool's actual host imports and signatures. It flags partial
strand.map outcomes and names granted peer
messages and background-only workflow steps when their modules are offered.
Version 9 tells the agent to use `fs_read` at `cap://` and
`cap://<module>` before writing a code-mode program, and names `job://` as a
polling path. The `code_mode` and `fs_read` tool descriptions supply the full
signature and cursor semantics. The installed tool descriptions determine
which namespaces a host serves.
## Semantic query guidance
Version 14 directs agents to `cap/lsp` in code mode for semantic questions
when that module is offered, and to `cap/lsp_sql` for joins, counts and filters
on explicit observations when its separate module is offered. The prompt
requires reading the corresponding `cap://` API first. Installed naming hints
travel with code-mode discovery, and rename retains a judgment step between
preview and a separate apply call. Source-grounded paths and lines, unsupported
request handling, server-failure diagnosis and retry discipline remain explicit.
Existing session pins are not migrated; fresh sessions receive the new pack.