The Single-Prompt Miracle and What It Cost
Who this is for — anyone who has watched an agent produce a working application from one prompt and wondered what they just accepted.
Read this if — you build from templates, or you are about to ship first-shot generated code to a real user.
Skip this if — you never reuse a sibling project as a starting point, and every generated line you keep has already been read.Takeaways
- A single prompt produces an application only when a sibling project has already answered the structural questions — the speed comes from the template, not from the model.
- First-shot code’s dangerous defects are the ones that report success: a fallback, not a crash.
- Inheritance is a loan. It arrives with the template’s documentation, its assumptions, and its packaging bets, and those come due later.
pdf-doctor is a GTK4 application and a GNOME Files (Nautilus) context-menu
extension that merges PDFs. It exists because of a tax deadline. It was built
in one prompt, it worked, and it was wrong in three ways that no amount of
running it would have shown.
This chapter is the audit, not the demo.
On Evidence
What is real here, and what is reconstructed. Every file path, commit hash, code excerpt, issue file, and file count below is taken from the
pdf-doctorrepository and can be checked withgit show <hash>or by running the command shown. The prompt in §1 is reconstructed — Git records what changed, not what was typed — and is labelled illustrative where it appears. No dialogue in this chapter is presented as a transcript.
1. One Prompt, One Application
The problem was ordinary: a folder of receipts and statements that had to
become a few categorised PDF bundles, some with specific page ranges, before a
filing deadline. Uploading financial documents to a web converter was out. A
multi-megabyte PDF suite was slower than the task. Hand-typing pdfunite
invocations for every combination was the thing being avoided.
What was missing was ten seconds of workflow: select PDFs in Nautilus, right-click, Merge…, adjust ranges, save.
Illustrative prompt — reconstructed, not a transcript. The load-bearing
clause is “using ../spriteview as the template”; everything after it is a
feature list:
Build
pdf-doctorusing../spriteviewas the template: a GTK4 app plus a Nautilus extension that offers Merge… when two or more PDFs are selected, with drag-and-drop reordering, per-file page ranges (1-3, 5, 8-12), an engine choice, a headless--mergeCLI, and an installer.
One commit came back:
$ git show --stat 9c6f28e | tail -3
tests/test_pdf_merge.py | 45 +++
website/index.html | 427 +++++++++++++++++++++
42 files changed, 3796 insertions(+)
It installed. Nautilus showed Merge… on a two-PDF selection. The bundles were built and the filing went out.
The interesting question is not “how did the model do that.” It is what part of that did the model actually have to decide.
2. The Speed Came From the Template
spriteview is a sibling in the fleet — a GTK4 tool for inspecting sprite
sheets, with the same shape: a desktop window, a Nautilus entry point, a
single-file packing step, an installer. Four structural questions were already
answered there, and none of them were re-litigated:
| Question | Where spriteview had already answered it |
|---|---|
| How does a GTK4 window get driven from a Nautilus extension without stalling the file manager? | python3-nautilus integration pattern |
How does a multi-module Python app ship as one executable in ~/.local/bin? | scripts/pack.py |
| What are this codebase’s conventions? | AGENTS.md, docs/, the harnez block |
| What proves an install worked? | make test, make install, make test-desktop |
The prompt did not ask for a desktop application architecture. It asked for a domain-engine swap: sprite rendering out, PDF page slicing in. That is a bounded, mechanical task with a known-good scaffold — the single best shape of work to hand an agent, and the reason the output looked like a miracle instead of like a pile of broken C bindings.
State that plainly, because it is the transferable part: single-prompt
application generation is template instantiation. Without spriteview in the
sentence, the same prompt is a three-day project with a different failure mode
every hour.
3. The Bill: Three Defects That Reported Success
Once the deadline passed, the generated code was read rather than run. Three
issue files came out of that reading — 001, 002, and 003, now in
issues/archive/. All three share one property: running the app could not
have found them, because in each case the wrong behaviour returns a success.
The first is the one worth printing. Here is parse_page_range() as it
shipped in 9c6f28e, ending with:
return pages if pages else list(range(1, max_pages + 1))
Read that line as a user of the feature. You typed 1-3 into a page-range
field on a five-page bank statement. If your input parsed, you get pages one to
three. If anything about your input failed to parse — a typo, a range past the
end of the document, a stray character — pages is empty, and you get all
five pages, silently, with a success dialog. The body of the function is
built the same way: every parse error is except ValueError: pass, and
out-of-range bounds are clamped into range rather than rejected.
merge_pdfs() compounded it one level up. If the selection engine was
unavailable or failed, control fell through to the whole-file pdfunite/gs
path — which cannot express a page selection at all, and therefore merged
complete documents.
In a tax filing, the pages you deliberately excluded are the ones you most
needed excluded. issues/archive/001-preserve-requested-page-selection.md
states the principle in one sentence:
Returning a successful document with extra pages is worse than returning an error.
The fix in 0a31c9a inverts the default: every malformed token raises,
out-of-range pages raise instead of clamping, the input/range list lengths must
match, engine arguments are built from normalised page numbers rather than
from raw user text, and a custom selection that cannot be honoured stops with
an error. Two more followed. e674191 made publication safe — engines write a
temporary file, the result is validated (non-empty, %PDF- magic) and only
then published with os.replace(), and an output path that aliases an input is
rejected up front, closing a path where the merge could destroy its own source.
29ac23f made the installer create the directories it had assumed already
existed.
🤖 Agent takes the wheel — turning each defect into a table of rejected inputs and making it pass.
test_parse_page_range_rejects_invalid_inputsubtests"abc","0","6","1,,2","1--3","2-6", and"-"against a five-page document, andtest_custom_ranges_never_fall_back_to_whole_file_enginescollects every binarysubprocess.runwas handed and asserts each one ends inqpdforpdftk.🛡️ The check that makes it safe —
make testinpdf-doctor, which runs the syntax check, the unit tests, and the REUSE lint. The structural assertion is the important one: it checks which binary was invoked, not the returned message, so a whole-file engine can never again service a custom selection.
4. The Fix That Removed the Feature
Fail-closed was right, and it broke the feature for almost everyone.
Custom ranges were routed exclusively through pdftk. That was survivable
while the code silently fell back to merging whole files — the user got a wrong
document, but they got a document. With the fallback correctly removed, every
page-selection merge on a machine without pdftk now stopped with an error.
pdftk is not in Fedora’s repositories; it needs pdftk-java from a separate
source. An advertised headline feature had become unreachable on a stock
install — and it had always been unreachable there. Removing the fallback did
not break it; it stopped hiding that it was broken.
8b2a401 says so in its own message:
Since page selection correctly fails closed, the feature was unreachable without manually installing pdftk.
The repair adds qpdf — which ships in the core repositories of Fedora,
Debian/Ubuntu and Arch, and has no 26-input handle limit — as the preferred
selection engine, with pdftk as fallback and an error only when neither
exists.
Then the same session nearly re-broke it. The pdf_engine setting names a
whole-file engine (auto, pdfunite, gs). Filtering the selection engines
by that setting means anyone who had chosen pdfunite in preferences silently
lost page selection and was told no engine was installed. docs/PDF_Engines.md
records both the trap and the two-line shape that avoids it:
if engine in ("qpdf", "pdftk"):
selection_engines = (engine,)
else:
selection_engines = ("qpdf", "pdftk")
🧑✈️ Human holds the wheel — deciding that a correct error is still a broken feature. Nothing in the suite was red; every test of the fail-closed behaviour passed, on a machine where the maintainer had
pdftk. What surfaced it was someone asking what happens on a fresh Fedora box. Packaging reality is a judgement call about users, and no assertion in the repository holds it.
This is the same failure as
The Canary That Skipped Itself, arriving from the
other direction. There, one machine’s foot version was mistaken for every
machine’s; here, one machine’s package set was. Rule V2 says to probe the
binary that will actually run — its sibling is that the machine which will
actually run it is not yours.
And it is the beat the “miracle” framing hides. The generated code was not merely incomplete — it had made a packaging bet nobody had noticed anyone making, and the bet only became visible once a correctness fix stopped papering over it.
5. Inheritance Is a Loan
The template gave the project its architecture. It also gave it sixteen documentation files describing another project.
That initial commit carried docs/Packaging.md, docs/Interactive_Crop_And_Save.md,
docs/In_Place_Navigation_And_Application_Uniqueness.md and thirteen more —
all seeded from spriteview, several describing features pdf-doctor does not
have and never will. docs/README.md was still titled “Sprite View - Documentation Index”
and linked every entry with an absolute file:// URL, so the index of one
repository pointed into another.
issues/005-clean-inherited-spriteview-docs.md is still open, and its
motivation is the general form of the cost:
Stale docs are worse than missing docs: an agent or contributor reading
Packaging.md(14 Sprite View references) cannot tell which statements still hold for PDF Doctor.
Note who the victim is. The documentation an agent reads to learn the project
is the documentation most likely to have been inherited wholesale, because
prose is what a template swap does not touch. Nine files under docs/ still
mention the wrong project — grep -ril sprite docs/ counts them — and the
index is the only one that has been rewritten.
The loan came due in a second currency too. 05df98a exists because a stale
installed copy of the extension made the running behaviour disagree with the
source — “a failure that cost three debugging rounds this session” — and its
fix, scripts/check-install.sh, now compares the installed extension byte for
byte against dist/pdf_doctor.py and reports when Nautilus started before the
extension was installed.
🤖 Agent takes the wheel — the byte comparison and the process-start-time check. Both are mechanical, and both encode a symptom that had already burned an afternoon.
🛡️ The check that makes it safe —
make check-install, which fails when the installed artifacts differ from the packed build, and warns when no page selection engine is present at all.
6. The Guardrail Was the Issue File, Not the Harness
The three fixes in §3 were done as an experiment: a different CLI harness (Goose) driving a different vendor’s model, on a codebase neither had seen. The harness experience was rough — verbose, awkward interactive loops, no familiarity with the fleet’s conventions.
The fixes landed correctly anyway, and the reason is checkable. Each one was
written as a specification file before execution, in the four-part shape
CONTRIBUTING.md prescribes:
| Section | What it pins down |
|---|---|
| Summary | the exact defect, in one paragraph |
| Motivation | why the current behaviour is unacceptable — here, silent page leaking |
| Proposed Solution | the architectural constraints: fail closed, os.replace(), create the directory |
| Alternatives Considered | what is explicitly forbidden — “keeping the whole-file fallback was rejected” |
The fourth section is the one that does the work. An agent that has been told what is forbidden cannot rediscover it as a clever simplification. Issue 001’s alternatives section names both the whole-file fallback and silent clamping, which are precisely the two shapes the original code had.
The experiment left one honest scar in the record. This book’s rule L3 (see the
Rule Index) says to record the model in a Co-Authored-By:
trailer. The Goose commits carry none:
$ git log --format='%h %s%n %(trailers:key=Co-authored-by,valueonly)' --reverse | head -8
9c6f28e feat: initial commit for pdf-doctor GTK4 app and Nautilus extension
0a31c9a fix(merge): preserve requested page selections
e674191 fix(merge): publish output safely
29ac23f fix(install): make repository setup reliable
Every commit after those four carries a trailer. The convention held wherever the usual harness ran and lapsed the moment it did not — which is the accurate version of “the tooling enforces it”: it does not. A person did, habitually, in one harness and not the other.
7. The Rules This Earned
Four, and they are all about the gap between working and correct.
Read the fallbacks first. In generated code, the paths that crash are the
safe ones — someone will notice. Budget the review for every branch that
returns success without doing what was asked. In pdf-doctor there were two,
stacked, and one line of code held them both.
A correct error is still a broken feature. Making an operation fail closed changes who can use it. Check reachability on a stock install in the same session, or the fix ships as an outage.
Write the spec before you hand over the work, especially to an unfamiliar model or harness. The Alternatives Considered section is not documentation politeness; it is the fence.
Audit what the template brought. Code gets swapped, prose gets inherited,
and inherited prose is what the next agent reads as truth. Two weeks after its
first commit, pdf-doctor still carries nine documentation files about a
sprite viewer.
The last one is the debt pdf-doctor has not finished paying. It is filed,
open, and cited here rather than quietly omitted, because a chapter that
audits an agent’s output and hides its own outstanding balance is not an audit.
Full history is in pdf-doctor: issues/archive/001–003 for the defects
above, issues/004 and issues/005 for what is still open, and
docs/PDF_Engines.md for the engine-selection reasoning.