The agent reports that the client is finished. The file parses, every export exists, and the tests you never mentioned fail on their first run. The prompt named the API and the language and left the rest to be inferred, and the agent inferred that done means compiles.
What you get
You will end up with a task prompt naming the spec, auth scheme, language, test command, and done criteria, with a linter for the prompt and a checker for the result. This is for you if you hand integrations to a coding agent one at a time.
Short answer
Write one prompt per integration with five named parts: the spec path, the auth scheme, the target language and file, the test command, and the done criteria. Make every criterion something you can run or read, and put the test command in it. A plan-first flow costs one round trip and catches scope drift before code exists. Check the agent’s final message against the criteria, then run the test command yourself.
You will need
A coding agent that is installed and signed in, and Node 22 or later for the sample.
Verified 2026-09-25 against Node 22.22.2. The prompt here is the per-task one, not the
repository’s instruction file. The
Claude Code guidance puts what is true of
every task in CLAUDE.md. This prompt carries what is true of one integration, and it goes
in the first message of the session.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A single detailed prompt | A task whose operations and files you can list yourself before the agent starts | Every gap in the prompt is filled by the agent’s guess, and you meet the guess in the diff | You cannot name the files the change will touch |
| Aider architect mode | Aider, and a change you want proposed by one model and applied by another | Two models per message, and the proposal is applied without a pause unless --auto-accept-architect is turned off | You want to read the plan before any edit happens |
| Claude Code plan mode | Claude Code, and a change across several files or in code you do not know | One extra round trip and a plan to read, which its own documentation says to skip for a change you can describe in a sentence | The scope is clear and the fix is small |
| Gemini CLI plan mode | Gemini CLI, and research you want done read-only before any file changes | A plan written to a Markdown file for you to approve or edit, with only read tools allowed until you do | A one-line change, or a session that already knows the code |
A single prompt is the cheapest flow and the one where every omission becomes a guess. Plan-first flows add a round trip. Claude Code and Gemini CLI both block edits until a plan is approved, and Aider’s architect mode has a proposer and an editor but applies the proposal by default.1 The round trip buys you the operation list and the file list before any code exists, which is where scope drift is cheapest to catch.
Name the five things the agent would otherwise guess
The prompt is a Markdown file with five headings. Each one closes a gap that the agent would otherwise fill on its own, and the last one is the one that gets left out.
## Spec
`openapi.json` in this directory. Implement every operation under `paths`: `listReadings`,
`getReading` and `createReading`. Do not add an operation the document does not have, and do
not change the document.
## Auth
The `apiKey` security scheme: every request carries the key in the `X-API-Key` header. The
client takes the key as a constructor argument and never logs it or puts it in a URL.
## Language
JavaScript on Node 22, ES modules, the runtime's `fetch`. One file, `client.mjs`, exporting
`createClient({ baseUrl, apiKey })`. No new dependencies.
## Test command
`node --test client.test.mjs`. The tests start `mock-api.mjs` on a free port and run the
client against it. Run them yourself before you report; do not edit them.
## Done when
- `node --test client.test.mjs` exits 0.
- `client.mjs` exports `createClient`, and the object it returns has `listReadings`, `getReading` and `createReading`.
- `package.json` has no dependencies.
- A response outside the 2xx range is thrown as an error whose message names the method, the path and the status.
- Your final message lists each operation implemented and pastes the last twelve lines of the test output.
The spec section names a path and the operations in it, and it forbids inventing more. The
auth section names the scheme by the key the document uses, apiKey, and says where the
credential comes from. The language section names the runtime, the module system, the one
file to write, and the dependency budget, which is zero. The test command is a command, in
backticks, that the agent runs and you run. Anthropic’s own guidance on
being clear and direct
is the rule behind all five: say what you want, and say it in terms a colleague with no
context could follow.2
The done criteria are the part that decides where the agent stops. Three of the five are commands or files a script can check. The fourth is behavior the tests exercise. The fifth asks for evidence in the final message, which is what you read first.
A prompt linter holds the file to that shape before it is sent. It checks that the spec path exists and parses as OpenAPI, and that the auth section names a scheme the document declares. It reads JSON only, because Node has no YAML parser built in and the prompt allows no dependencies. A YAML spec gets a finding rather than a pass. It checks that the test command’s executable is on the PATH, and that no criterion rests on a word like “works”:
if (s['done when']?.trim()) {
const items = bullets(s['done when'])
if (items.length < 2) findings.push('done when: fewer than two criteria')
if (testCommand && !items.some((b) => b.includes(testCommand))) findings.push('done when: no criterion names the test command')
for (const b of items) {
const m = IMPLICIT.exec(b)
if (m) findings.push(`done when: "${b}" is not checkable, "${m[0]}" describes a feeling`)
}
}
Run against the prompt above and against the kind of prompt that produces a client that compiles:
node demo.mjs
task-prompt.md
5 sections present, every criterion checkable
vague-prompt.md
no "spec" section
no "auth" section
no "language" section
no "test command" section
done when: "The client works correctly." is not checkable, "works" describes a feeling
done when: "Auth is handled as expected." is not checkable, "handled" describes a feeling
The vague prompt is eleven lines and reads fine. Every finding against it is a decision the agent was about to make instead of you.
Ask for a plan first when the scope is not yours to state
When you cannot list the files a change will touch, spend one round trip on a plan. Start Claude Code in plan mode and send the same prompt:
claude --permission-mode plan
Claude reads files and proposes a plan and makes no edit until the plan is approved. Read
the plan against the prompt. The operations it lists are the three under paths and no
more, the files it will write are client.mjs and nothing else, and the test command it
names is yours. A plan that adds a retry helper or a fourth operation is scope drift caught
for the price of one message. Gemini CLI does the same with
gemini --approval-mode=plan, writing the plan
to a Markdown file you can edit before approving it.3 Aider’s
architect mode has a proposing model and an
editing model, and pauses between them only when told to:
aider --architect --no-auto-accept-architect --test-cmd "node --test client.test.mjs" --auto-test
The --test-cmd and --auto-test switches
make Aider run your test command after every edit and try to fix a non-zero exit, which
turns the second criterion into the agent’s own loop rather than yours.
Check the result against the criteria, not the message
The agent’s final message says done. The checker reads the same “Done when” list and runs
what it can. A criterion that starts with a command is run and its exit code read. The
checker splits the command the way a shell does, honoring quotes and backslash escapes, and
runs it without a shell, so variables and globs pass through unexpanded.
Node 23 changed the default reporter for
piped output from TAP to spec, so the checker reads the pass and fail counts in either
format. A criterion that names exports is checked by importing the file, and one about
package.json by reading it, so a file that is missing or does not parse fails. Anything
else is listed for you to check against the message.
if ((m = RUNS.exec(item))) {
// Split as a shell splits, quotes and backslashes honored, then run with no shell, so
// nothing is expanded. A quote left open fails here as it would fail in a shell.
const words = argv(m[1])
if (!words) {
results.push({ item, ok: false, detail: 'the command leaves a quote open' })
continue
}
const [exe, ...args] = words
// When this runs inside `node --test`, the runner marks the environment, and a nested
// runner that inherits the mark reports as a subtest and exits 0 whatever happened.
const { NODE_TEST_CONTEXT, ...childEnv } = env
const run = spawnSync(exe, args, { cwd: dir, env: childEnv, encoding: 'utf8' })
// A program that cannot start, most often one not on the PATH, fails the criterion.
if (run.error) {
results.push({ item, ok: false, detail: `${exe} did not start: ${run.error.code ?? run.error.message}` })
continue
}
// Piped, Node 22 reports in TAP, `# pass 1`, and Node 23 and later in the spec format,
// `ℹ pass 1`. Both are read, and the counts are reported in TAP's form either way.
const summary = (run.stdout + run.stderr).split('\n').map((l) => l.replace(/^ℹ /, '# ')).filter((l) => /^# (pass|fail) /.test(l)).join(', ') || `exit ${run.status}`
results.push({ item, ok: run.status === 0, detail: `exit ${run.status}, ${summary}` })
The demo runs it twice: against client-stub.mjs, which is where an agent stops when done
was left implicit, and against the finished client.mjs.
node demo.mjs
done criteria against the stub an agent stopped at
FAIL `node --test client.test.mjs` exits 0. exit 1, # pass 1, # fail 4
ok `client.mjs` exports `createClient`, and the object it retur 4 names present
ok `package.json` has no dependencies. no dependencies
hand A response outside the 2xx range is thrown as an error whose check by hand against the final message
hand Your final message lists each operation implemented and past check by hand against the final message
not done
done criteria against the finished client
ok `node --test client.test.mjs` exits 0. exit 0, # pass 5, # fail 0
ok `client.mjs` exports `createClient`, and the object it retur 4 names present
ok `package.json` has no dependencies. no dependencies
hand A response outside the 2xx range is thrown as an error whose check by hand against the final message
hand Your final message lists each operation implemented and past check by hand against the final message
done, pending the by-hand items
The stub passes two of the three checkable criteria. A prompt without a test command would have asked for no more than the stub delivers: it parses, it imports, and all four names are present. One test of five passes against it. That is the gap between compiles and done, as a number.
For the by-hand items, take the final message from the agent rather than from your memory
of it. In Claude Code, claude -p with
--output-format json returns it in the result field, so the paste of the test output the
fifth criterion asked for can be diffed against the output you produced yourself.
Check it worked
Thirteen tests cover the linter, the checker, and the mock API. The literal that matters is # pass 1, # fail 4
against the stub: a checker that reported the stub as done would be the failure this page
exists to prevent.
node --test prompt.test.mjs
1..13
# tests 13
# suites 0
# pass 13
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2000.781847
One test writes a client.mjs with a missing export and a package.json with a dependency.
It asserts the checker fails both criteria, naming what is missing and counting what
appeared. Another runs the linter and the checker from a directory with a space in its path
and expects exit status 1 from each. A script that compared its own path with a URL there
would skip its main block and exit 0, and a checker that exits 0 reads as done.
When it goes wrong
The agent stops at a client that compiles. The done criteria were implicit, or absent. Run the linter: a criterion it calls “not checkable” is one the agent got to interpret, and “works” is always read as “runs without throwing.”
The tests pass and the client is wrong. The agent edited the tests. The prompt says not to,
and the exit code cannot tell. Diff client.test.mjs against the commit before the session
before you trust the number.
The plan lists an operation the spec does not have. The spec section forbids it, and the plan is the cheapest place to say no. Reject the plan, quote the section, and ask again.
Aider applied the plan before you read it. --auto-accept-architect is on by default, and
--no-auto-accept-architect is the switch that makes architect mode pause.
The final message pastes test output that does not match yours. The agent ran the tests at a point before its last edit, or ran a subset. The test command in the criteria is the one you run; the paste is evidence to compare, not a result to accept.
When not to do this
Do not put the per-task prompt into the repository’s instruction file. CLAUDE.md,
GEMINI.md, and Aider’s conventions file are read on every session, and a done list for one
integration becomes noise for the next fifty. The instruction file holds the house rules;
the task prompt holds this task.
Do not ask for a plan on a change you could describe in one sentence. Claude Code’s own guidance says so, and a plan for a one-line fix is a round trip that buys nothing.
Do not let the agent write its own done criteria. It will write ones it can meet, and “compiles” is the one it can always meet. The criteria are yours, they are in the prompt before the first token comes back, and they do not move during the session.
Do not treat a pasted test summary as a test run. The paste is there so you can compare it with the run you make yourself. A paste with no run behind it is the failure mode the fifth criterion exists to expose. Run the command.
Related how-tos
Last verified
Verified 2026-09-25 against Node 22.22.2. Every output block is what the command preceding
it printed. No agent was run to produce the output: client-stub.mjs is a hand-written
stand-in for the state an agent leaves a client in, and client.mjs is the finished client
the prompt asks for. The plan-mode and architect-mode commands are the ones each tool’s
documentation gives.
Footnotes
-
Aider’s options reference lists
--auto-accept-architectwith a default of True. So the mode named for the person who draws the plans hands them to the builder without a pause, unless the flag is negated. The modes page describes an architect model that proposes and an editor model that translates the proposal into edits, which is two models agreeing with each other by default. ↩︎ Back to text -
The prompting guide offers what it calls a golden rule. Show your prompt to a colleague with minimal context on the task and ask them to follow it; if they would be confused, so will the model. The five sections here are what that colleague would ask for, in the order they would ask. ↩︎ Back to text
-
Gemini CLI’s headless reference gives the process four exit codes: 0 for success, 1 for a general error, 42 for an input error, and 53 for a turn limit exceeded. A coding agent that reports running out of turns as a distinct exit code has met the session that never finishes, and has given it a number. ↩︎ Back to text