Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. 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.
  2. First-shot code’s dangerous defects are the ones that report success: a fallback, not a crash.
  3. 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-doctor repository and can be checked with git 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-doctor using ../spriteview as 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 --merge CLI, 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:

QuestionWhere 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_input subtests "abc", "0", "6", "1,,2", "1--3", "2-6", and "-" against a five-page document, and test_custom_ranges_never_fall_back_to_whole_file_engines collects every binary subprocess.run was handed and asserts each one ends in qpdf or pdftk.

🛡️ The check that makes it safe — make test in pdf-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:

SectionWhat it pins down
Summarythe exact defect, in one paragraph
Motivationwhy the current behaviour is unacceptable — here, silent page leaking
Proposed Solutionthe architectural constraints: fail closed, os.replace(), create the directory
Alternatives Consideredwhat 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.