Skip to content

Read before you write: the habits that keep an AI agent honest

A coding agent is most dangerous when it writes against a fact it has not looked at.

That sounds obvious until you watch the failure happen. The agent remembers that there is a dev script, so it runs the wrong wrapper. It remembers that a ticket failed, so it edits the ticket before reading the run state. It remembers that Tempo is the Go rewrite of the orchestrator, so it describes Tempo as if the full lifecycle had already moved, even though Tempo's own files separate ported commands, static-runner defaults, and parity gaps.

The fix is not a more poetic instruction. The fix is a work habit: read the artifact that owns the fact before you write the sentence, command, test, or rule that depends on that fact.

In this repo, that habit shows up most clearly in the orchestrator work. The orchestrator is a pile of boring contracts: ticket markdown, run-record markdown, state JSON, project config, shell wrappers, lint gates, and now the Tempo Go port. They are not glamorous. They are where the truth lives.

Read the ticket and the run record before touching the work

Every schedulable job starts as a ticket under tickets/backlog/ or a phase directory. The ticket says what was requested. Its sibling *.run.md says what actually happened: surprises, dead ends, outcome, and the parts of the run that do not fit in a diff.

That split is why the project rule for a failed run is so strict. If a ticket fails, stalls, times out, or escalates, the next agent is not allowed to start by editing code or retrying. It must first diagnose the run with the dispatcher:

bash workflow/scripts/dev.sh diagnose-run <run-id>

Then it has to read the run record and the .state/runs/*.json cause field, name the root cause in one sentence, and only then change anything.

This is read-before-write in its plainest form. A failed run is evidence. If the agent edits first, it is treating the failure as a vibe. Maybe the ticket was wrong. Maybe the environment was broken. Maybe the run hit a provider limit. Maybe the implementation changed the wrong file. Those are different problems, and the repo already recorded the clues that tell them apart.

The same rule explains the history-index workflow. When the question is "why did we choose this?" or "what happened last time?", the first sources are not git log and scattered grep hits. They are the ticket, the sibling run record, and then .state/history.duckdb, the derived DuckDB index over tickets, run records, state files, git history, and cached PR data. The command surface is explicit:

bash workflow/scripts/dev.sh history "<question>"
bash workflow/scripts/dev.sh history-search --limit 5 "query words"
bash workflow/scripts/dev.sh history-query "SELECT ..."

Git still matters, but it is the fallback for provenance questions, not the starting point. The commit can tell you what changed. The run record can tell you what the agent learned, what failed first, and what judgment call it made.

Read the wrapper before running the command

This project has two dev.sh wrappers, and they do different jobs.

scripts/dev.sh is the project wrapper. It owns local setup, app startup, tests, lint, doc checks, and the review checks that run against the product repo.

workflow/scripts/dev.sh is the orchestration dispatcher. It owns ticket runs, review, ship, backlog listing, run diagnosis, history queries, and the other workflow verbs.

An agent that writes "run dev" from memory is already in trouble. The current rules say the same thing more sharply: do not hunt the filesystem for executables. A tool may be invoked only through an activated venv/bin/<bin> or as a bare command on PATH. If the tool is missing in an autonomous run, run:

bash scripts/dev.sh setup

Do not probe /usr/bin, /usr/local, ~/.local, or some other install you happen to find. That workaround can make a green check meaningless, because it ran with dependencies the project does not use.

Tempo carries the same habit into the Go port. The Tempo dev wrapper downloads the pinned Go toolchain into tempo/.toolchain/go/, sets GOTOOLCHAIN=local, and refuses to run build, check, test, or parity until that repo-local toolchain exists. It also creates tempo/.venv for Tempo-owned Python code and rejects the surrounding repository's venv. That is not ceremony. It means a Tempo check uses the toolchain Tempo declares, not the toolchain an agent found by poking around the machine.

Before writing a command into a ticket, a blog post, or a runbook, read the wrapper that owns it. The command is a contract, not a memory exercise.

Read the config before claiming what the gate does

Rules drift unless a check makes them visible.

In this repo, review checks are not folklore. They live in workflow/project.config.json. The current always-on review list includes, among many others, lint-devblog-mirror, lint-run-records, lint-no-direct-random, lint-no-handcrafted-uistate, tempo-check, test, and docs-build-strict. Diff-scoped checks include lint-doc-tags. Soft checks include lint-dedup-drift, lint-context-manifest, and tempo-parity.

Those details matter. If you say "the review blocks on X" without reading the config, you may be wrong in either direction. A soft check can self-report drift without blocking the merge. A diff-scoped check may run only when the touched paths make it relevant. An always check runs on every review. Those are three different contracts.

The project has a process-contract rule for exactly this reason. If a ticket adds, removes, or renames a preflight verb in workflow/project.config.json, it must update the config entry, the scripts/dev.sh case arm, the help text, and a workflow test in the same change. That rule is the config version of read-before-write: do not update one surface from memory and leave the other surfaces lying.

The portable lesson is simple. If a rule matters, find the thing that enforces it. If nothing enforces it, say so. If a command is listed as a gate, read the config and wrapper before describing its behavior.

Read Tempo's current boundary before writing about Tempo

Tempo is a good test of honesty because it is real and still bounded.

It is real: tempo/ is a Go module with a CLI, tests, a pinned local Go toolchain, a Tempo-owned Python runtime, backlog and history commands, lint-gate dispatch, codemap commands, bootstrap assets, a parity harness, release snapshot support, and a live ticket engine adapter.

It is bounded: the current parity report names what it does not prove. It covers selected read surfaces and deterministic lifecycle decisions with fake agent, GitHub, and git layers. It does not prove real agent output quality, live GitHub merge behavior, notification delivery, or full live bash ticket/review/ship execution. The default tempo ticket path also uses a deterministic static runner so tests do not depend on paid agent calls. The live adapter is opt-in with --engine, resolves the configured engine from the target project or PATH, and requires the engine to finish with a TEMPO_RESULT ... line.

Both sides are true. A stale post that calls Tempo "the orchestrator, rewritten in Go" without the boundary is too broad. A stale post that calls it "just a scaffold" misses the pieces that have landed since the original scaffold: backlog and history commands, codemap commands, bootstrap docs, parity fixtures, release packaging, secret loading, and the live runner contract.

The read-before-write habit is what keeps the sentence honest. Read the README. Read tempo/scripts/tempo-dev.sh. Read the parity description. Then write the claim at the level the code supports.

That is also why the workflow-to-Tempo mirror rule exists. Any change under workflow/ has to be mirrored into the corresponding Tempo surface when one exists, and bash tempo/scripts/tempo-dev.sh parity is the backstop. The parity check deliberately reports uncovered lifecycle surfaces so nobody mistakes a read-surface regression net for full orchestrator equivalence.

Read the real artifact, then specify the check

This habit is not only for docs. It is the safer way to write tests and gates.

If a test asserts an event name, read the log catalog or the real client log first. If a stage test needs UI state, drive it from a server-shaped fixture or selector instead of a hand-built object literal. If a lint claims to enforce a rule, read both the rule and the implementation before describing what it proves.

The older version of this post spent most of its time on those client examples. They are still valid, but the orchestrator examples make the broader point more clearly. A ticket, a run record, a state file, a wrapper, a config entry, and a parity report are all the same kind of thing: the artifact that owns the fact.

The loop is:

  1. Find the artifact that owns the fact.
  2. Read it.
  3. Write the command, test, doc, or rule against what it actually says.
  4. Add or run the check that makes drift visible.

Skipping step 2 is how agents invent plausible state, plausible commands, plausible event names, and plausible project history.

What this does not solve

Read-before-write is a defense against invented facts. It is not a replacement for judgment.

A run record can tell you why the last attempt failed; it cannot decide whether the product direction is good. lint-devblog-mirror can prove that a DevBlog file exists with the same slug and H1 as the Blog post; it cannot prove the summary is useful. tempo-parity can compare normalized fixture output; it cannot prove Tempo is ready to replace the whole shell orchestrator. A server-shaped fixture can prove the input shape is real; it cannot prove the assertion is worth making.

That limit is not a weakness in the habit. It is the line the habit is meant to draw. Read-before-write keeps the agent honest about facts. Judgment still has to decide what to do with them.

The practical standard is modest and strict: do not write a claim until the owner of that claim is in front of you. Read the ticket before implementing it. Read the run record before diagnosing it. Read the wrapper before running it. Read the config before citing the gate. Read Tempo before writing about Tempo.

Then write.


Or: subscribe to the newsletter for more posts on building this stuff.