The run record that grades itself¶
A normal audit trail answers what happened. This project needed a trail that also answers whether the work was estimated, routed, and reviewed well.
The basic artifact pair did not change. A ticket under tickets/ says what an
agent was asked to do. A sibling .run.md file says what happened. Git carries
the diff. The history index rebuilds .state/history.duckdb from those local
files. That is already enough to debug many failures.
The new part is calibration. The ticket now carries optional predictions made before execution. The run record carries measured fields and review outcomes after execution. The history index turns both into columns, so the repo can ask ordinary SQL questions about forecast, cost, context, and review churn.
The Prediction Is Captured Before The Run¶
Prediction fields live on the ticket because that is the only honest place to write them. After a branch is finished, the diff has already biased everyone's memory of how large the work looked.
The current optional ticket fields are:
workflow/docs/authoring/TICKET_AUTHORING.md defines the contract. The lint
checks shape, not accuracy: predicted_complexity is low, medium, or
high; predicted_files and predicted_minutes are non-negative integers.
Wrong predictions are allowed. They are the data the scheduler and future ticket
authors need.
The run-record contract in workflow/docs/authoring/RUN_RECORD.md echoes those
fields into the run record when they exist. That echo is deliberate. The
run_records table can answer calibration questions without joining back to the
ticket table:
SELECT
ticket_stem,
predicted_minutes,
actual_minutes,
actual_minutes - predicted_minutes AS minutes_delta
FROM run_records
WHERE predicted_minutes IS NOT NULL
AND actual_minutes IS NOT NULL
ORDER BY abs(minutes_delta) DESC
LIMIT 10;
That query is useful because both halves are recorded at the right time: expectation before the work, measurement after the work.
The Run Record Carries The Measured Outcome¶
Tempo and the workflow tooling stamp measured fields into run records instead
of asking the implementer to guess them. The current run_records schema in
workflow/scripts/history_index/schema.py includes:
started_atandended_atactual_filesandactual_minutespredicted_complexity,predicted_files, andpredicted_minutesbase_commitandmodel_exacttokens_in,tokens_out,tokens_total, provider-native token fields, and normalizedtoken_*fieldstool_callsandreview_iterationsfailure_mode,production_diff_lines,escalation_reason,surprises_text, anddead_ends_text
The measured fields are not all equally available. A degraded harvest may miss
token usage. A legacy run record may not have timestamps. A fresh clone may not
have local sidecars under .state/. The contract prefers nulls and explicit
omissions over invented values. That matters because this data is meant to train
future decisions, not make a dashboard look complete.
The same distinction applies to time. wall_time_estimate still exists as a
fallback for older records, but current timed runs use started_at and
ended_at as the measured source. Retros can still query old rows, while new
rows do not have to parse prose to know how long the run took.
Context Is A First-Class Signal¶
The run record says what happened, but training also needs to know what context the agent had when it acted.
There are two surfaces for that. During a run, the context-manifest harvester can
write a gitignored sidecar such as
.state/runs/<run-id>.<stage>.context.manifest.json. The history index ingests
those sidecars into context_manifests when they exist. The current table
stores the run id, stage, sidecar path, generated_at, source_log,
files_read, token rollups, tool_calls, base_commit, git_ref, and file
freshness fields.
The sidecar is useful local telemetry, but it is not the durable review
artifact. The committed artifact is the run record's ## Context manifest
section. That section carries a compact summary, for example:
## Context manifest
- files_read: 12
- tokens_in: 18234
- tokens_out: 2410
- tokens_total: 20644
- tool_calls: 19
- base_commit: abc1234
If the harvester cannot parse the stage log, the section records that instead of pretending it has data:
workflow/scripts/history_index/context_manifests.py also indexes committed
summary sections when they expose numeric fields. Sidecar rows win for the same
(run_id, stage) pair, and summary fallback rows make fresh review clones
queryable even when .state/ is absent.
Review Corrections Are Not Implementation Work¶
The run-record contract also has a ## Review corrections section. That keeps a
clean boundary between the first implementation and what review repaired.
Without that boundary, the final diff hides an important training label. A
branch that passes only after review fixes a stale heading, missing evidence, or
wrong field name is different from a branch that passes on the first review
iteration. Both may end with outcome: merged, but they should not teach the
same lesson.
The numeric companion is review_iterations. The prose companion is
## Review corrections. Together they let future analysis ask whether a ticket
shape, model route, context size, or rule surface tends to need review repair.
The Useful Queries Are Boring¶
The point is not clever SQL. The point is that the facts now exist in stable columns.
To find underestimated work:
SELECT
ticket_stem,
predicted_files,
actual_files,
predicted_minutes,
actual_minutes
FROM run_records
WHERE predicted_files IS NOT NULL
OR predicted_minutes IS NOT NULL
ORDER BY COALESCE(actual_minutes - predicted_minutes, 0) DESC;
To compare context size with review churn:
SELECT
rr.ticket_stem,
cm.files_read,
cm.tool_calls,
rr.review_iterations,
rr.outcome
FROM run_records AS rr
LEFT JOIN context_manifests AS cm
ON cm.run_id = rr.ticket_stem
AND cm.stage = 'run-record'
WHERE cm.files_read IS NOT NULL
ORDER BY cm.files_read DESC;
To group outcomes by exact model when the runner harvested it:
SELECT
model_exact,
outcome,
count(*) AS runs
FROM run_records
WHERE model_exact IS NOT NULL
GROUP BY model_exact, outcome
ORDER BY model_exact, outcome;
These are plain operations over local project memory. They do not require a SaaS tracker, a transcript search, or a human reconstructing intent after the fact.
The Feedback Loop Is Local¶
This is why the run record now grades itself. Not with a score, and not with a claim that the agent did a good job. It grades itself by preserving the fields needed to compare:
- what the ticket author expected
- what the run actually touched
- how long it took
- what context was read
- which model ran
- how much review had to fix
- which failure mode, if any, was recorded
That is enough for a future agent to learn from the run without trusting memory. It is also enough for a human maintainer to improve the next ticket. If a class of tickets is always underpredicted, split it earlier. If a rule is always missed despite being in context, improve the implementation path. If a context manifest is constantly absent, fix the harvester without blocking product work.
The important design choice is modest: keep the source of truth in committed markdown, derive the DuckDB index from it, and let missing metadata stay missing. A durable training signal is not the same as a perfect log. It is the set of facts that would be expensive or impossible to recover later.
Or: subscribe to the newsletter for more posts on building this stuff.