Writing prompts that hold
The prompt is source code, so it has to survive regeneration. Spec vs prose, tests as a gate, the machine's replies, and which sentences are load-bearing.
A .napl prompt is not a chat message you send once. It is source code: it will be
regenerated, diffed, and re-attributed every time it changes, and a fresh coding agent
will read it cold each time with no memory of the last run. A prompt holds when that
agent produces the same behavior every time and the toolchain can prove the connection.
This page is about writing prompts that hold: what to put where, what the toolchain
checks, and which of your sentences are actually load-bearing.
The body is a contract, not a description
The prose below the frontmatter is a specification handed to the agent. It should say what the code must do (the public API, the behavior, the edge cases) and leave every implementation choice to the agent. Name the functions and the shapes; describe the rules; enumerate the corners. Don't describe an algorithm you don't care about, and don't paste code: that's an implementation choice masquerading as a spec.
Every sentence here is a claim the agent must satisfy and the toolchain must be able to attribute to real lines of code. "Never silently renewed" is not decoration: it forbids a whole class of implementation. Vague sentences produce vague code and get flagged (see the machine's replies); precise ones become the spec.
Tests are data, and they gate
Behavior that can be pinned as an input/output pair belongs in tests:, not in the prose.
Each entry is a name, a given, and an expect: plain data, not code. The agent
iterates until every case passes, or gen fails loudly after three attempts. This is what
separates NAPL from "AI wrote some code": the tests are part of the source, they travel
with the prompt, and no generation is accepted until they're green.
Prose and tests divide the work cleanly:
- Frontmatter tests pin behavior expressible as data: a given input maps to an expected output or error.
- Prose carries everything the data can't: invariants, ordering guarantees, what is forbidden, why an edge case exists.
If you find yourself writing "returns X for input Y" in the prose, move it to tests:
where the toolchain can enforce it mechanically.
The machine answers back
napl gen writes a reply in the margin of your prompt: a .mapl file, regenerated on
every compile. It is how the toolchain tells you your prompt was ambiguous, what it had to
assume, and why the code is shaped the way it is:
- ambiguity: your words were unclear. Surfaces as a red squiggle on the exact words, like a syntax error, usually with a suggested rewording.
- assumption: a decision your prose left open. A warning you can either accept or close by making the choice explicit.
- no-op: the prompt changed but the code didn't. The entry must say why, or the gen fails. A changed prompt that quietly reports "clean" is a bug the toolchain refuses to ship.
- note: hover-visible reasoning about why the code came out this way.
- promptLines: [12, 14]
kind: assumption
message: Return type left unspecified
reasoning: >
The prose does not name the return type. Chose a plain string.Reading these back is how you tighten a prompt: an assumption you didn't intend is a
sentence you need to add. The full kind/severity table is in the
file formats reference.
Deletable sentences and load-bearing sentences
The hardest part of prompt-writing is knowing which sentences carry weight. A sentence is deletable if the toolchain already enforces its content some other way. It is load-bearing if deleting it changes what the agent produces. The two look identical on the page; the difference only shows up on regeneration.
The clearest case comes from dependencies. When one module builds on another, its
frontmatter declares deps:, and the gen loop surfaces that to the agent as a
Declared dependencies: … line automatically. So a prompt sentence that merely restates
the dependency:
Add a path dependency on
../schemas_line_rangein yourCargo.toml; it is a workspace sibling, not a crates.io dependency.
That sentence is redundant. The deps: frontmatter already carries the where. It's deletable.
But the sentence that tells the agent how to relate to that dependency:
Use its public API. Do not reimplement its types or logic.
That sentence is spec. It is the only thing standing between the agent and a local reimplementation. Delete it and a fresh agent, reading the prompt cold, will happily write its own copy of the sibling's types. This one is load-bearing.
The war story
This distinction was not theoretical. While the toolchain was generating its own source, a
pass over the prompts trimmed the redundant dependency prose, correctly in most cases. But on the
incremental module the trim was too aggressive and deleted the whole imperative,
"Add a path dependency on each in your Cargo.toml… use their public API, and do not
reimplement their logic or depend on any hand-written crate," leaving only two bullets
that described the sibling types.
On the next regeneration (journal gen #93), the agent did exactly what an unconstrained
prompt invites: it reimplemented LineRange and AttributionEntry locally and emptied
its Cargo.toml dependencies. The code compiled. The tests passed. Nothing was red, and
the module had silently stopped composing on the rest of the toolchain. What caught it was
the expected-no-op discipline: a prose trim should regenerate as a no-op, and this regen
produced patches, so the run halted and the diff got read. The fix was to revert the
regen and restore the imperative, in compact form, to the nine prompts that had lost it.
Here is the module as it stands, imperative intact: the prose that makes it hold is the "do not reimplement its types or logic" line:
The reusable lesson: a prompt that only describes a dependency invites reimplementation; a prompt must instruct the agent to depend on it. When you trim a prompt, ask of every sentence: does the toolchain enforce this already, or is this the only place it's said? Keep the second kind.
Next: Drift, moves & reconcile: what happens to a prompt's output after it's locked, and how to change generated code the sanctioned way.