recipes · v1.0.1

The daily progress report that checks itself — derived numbers, a page for people, a record the knowledge base ingests

verified 16.07.2026from internal-harness

A recurring report is read by people who cannot verify a single figure in it, and later by machines that cannot parse the prose it is written in.

sporo recipe11 sections · 9 scars · sealed 1.0.1
download .md ↓
the 11 gated sections
01The problem
02Why the obvious approach fails
03The principles
04The ground it needs
05The contracts5 bound
06The build sequence
07The seams
08The scars9 scars
09Verification
10The trade-offs
11For the human
what you'd hand an agent
11
gated sections
9
earned scars
5
bound contracts
1
self-contained file

Readable before it runs — audit the prose, then let an agent build it on your stack.

What a reader receives is the exported form — this, with the adoption protocol appended. Every section below was distilled from one live implementation: its build record, its release history, its fix commits, and the scars only a real build leaves.

Works with any agent
Claude CodeOpenAI CodexCursorGitHub CopilotGoogle GeminiWindsurf
the recipe, section by sectioneach gated · scars open
01

The problem

You ship work every day and the people who fund it, depend on it, or plan around it cannot see any of it. So somebody writes a summary. That summary is the one artifact your team produces that nobody checks: every other output is consumed by a machine that rejects it when it is wrong, and a report is consumed by a reader who has no repository to open. Now add a second reader who arrives later — a shared knowledge base that must answer "what shipped on that project last week, and who worked on it" — and prose alone stops being enough.

What you build: for each period, one durable record of derived facts, one narrative document carrying a machine-derived header, one self-contained page for people, one regenerated aggregate across every period ever reported, and one idempotent transmission of the period into a knowledge base that lives outside your repository and parses what you send.

The acceptance, in one sentence: every quantity on the page can be traced to the facts record a script derived (there is no third source), the period a reader saw yesterday is byte-for-byte the same document today, and re-sending a period changes nothing on the other side — while a period nobody could measure says so, on the page, instead of showing a zero.

02

Why the obvious approach fails

The next agent will do the obvious thing, and it is worth naming precisely because it produces something that looks right: ask a capable model to read the period's changes and write the report — counting the work items by the ticked boxes in their documents, rendering it to a page, and posting that page to the knowledge base.

Every part of that breaks, and none of the breaks is visible to the reader:

  • A recalled number is plausible, and plausible is indistinguishable from true. Asked how many changes landed, a model produces a number of the right magnitude. It is wrong, the reader cannot check it, and nothing in the pipeline ever disagrees with it.
  • Counting ticked boxes reports the opposite of the truth, not an approximation of it. In the verifying build, one live work item carried 0 of 41 ticked acceptance boxes in its documents while 7 of its 17 tracked phases were done. The box-counting report says 0% — to a stakeholder, on a real project, about work that was more than a third finished. Not imprecise: inverted, in the direction that destroys trust fastest.
  • Rendering first makes the page the source of truth, and the page is prose a model wrote. Everything downstream — the cumulative view, the header the knowledge base reads — then has to parse generated prose to recover numbers that existed, structured, thirty seconds earlier. A generated artifact that parses generated prose is a house of cards.
  • The consumer on the other side does not want your page. It wants a shape it can key, dedup, and index. Posting a document and hoping means every project on the fleet invents its own dialect, and the divergence is found by the consumer, live.

The fork: derive the facts into a durable record first, write the judgment against that record, and render both the page and the wire shape from it. Everything below is that inversion.

03

The principles

  1. The numbers are derived; the judgment is written; there is no third source. A script computes every quantity from the repository into a persisted record, and the prose cites that record. A number not in the record does not go on the page. (The genre's own division of labour: the machine gathers, the author abstracts.)
  2. Completion comes from the work items' own machine-readable state — never from a mark inside a human-written document. Boxes are not maintained by anyone; the state a check or a board already reads is.
  3. Every proxy carries its method into the document. A quantity that is not the thing it stands for ships with the sentence that says so, and the sentence lives inside the facts record — so the page cannot be rendered without the disclosure being available to state.
  4. Absence is a state, never a zero. A source that is missing, unreadable, or not kept on this machine is reported as an absence with its reason. A silent zero is a lie told with a number, and it is worse than the proxy it replaced.
  5. A total is not a measurement, and two questions never share one name. Concurrent effort summed is not elapsed time; volume handled is not volume produced. Where both are worth having, both are printed, each under its own name.
  6. A published period is immutable; the aggregate is regenerated. The period contains prose (irreproducible → frozen); the aggregate contains only what the facts say (reproducible → rebuilt, and gated like any other generated artifact). The aggregate is built from the facts and never by parsing the prose.
  7. A derived number is safe to embed; a derived string is not. Change subjects, titles and model-written headlines are untrusted the moment they reach markup, a structured header, or a wire body — regardless of how trustworthy their source is. Escaping is achieved by construction (a typed value handed to a serializer), never by remembering to escape.
  8. The wire shape belongs to its consumer, not to you. Outside the repository, field names are somebody else's parser. Copy the shape exactly, key it so that re-sending replaces rather than duplicates, and treat a change to it as a breaking change.
  9. A period is named explicitly, never inferred from a window's edge. A window and the day it describes are two different facts; deriving one from the other is how a record lands under the wrong key.
  10. A credential is read at use time and rides in the transport header only — never in the body, never in a log, never on disk. This is what lets the sender print its entire payload for inspection without leaking anything.
  11. Every artifact names which half of it was machine-derived, and which agent produced which half. Who sent a record and who built it are different facts; a reader who cannot tell them apart will diagnose the wrong component (see the scars).
  12. A gate that cries wolf is worse than no gate. A generated artifact carries no clock, and a drift check compares the substance the project can re-derive, never the shared skin it merely inlines.
04

The ground it needs

Five things must be standing before step one. Each is a ladder: probe for it, build the smallest one if it is missing, or degrade — and label the degradation where the report's own reader can see it. There is no fourth rung.

1. A machine-readable history of changes, with timestamps and authors

Why it is load-bearing: it is the only source in the building that cannot be flattered. Every delivery number and the effort proxy come from here.

  • Probe: anything that can answer "which changes landed between these two instants, by whom, touching what" without a human transcribing it.
  • Build the smallest: if changes are not recorded with timestamps, stop — this capability has no floor beneath it, and you are building a different thing first.
  • Degrade: there is no honest degradation. A report whose delivery numbers are typed is the failure this recipe exists to prevent.

2. A machine-readable state for the work items

Why it is load-bearing: progress is the number a stakeholder reads first, and principle 2 says it may not come from prose. Without this, the report has no progress at all — which is a supportable outcome, and inventing one is not.

  • Probe: look for state that something already acts on — a check that fails when it is wrong, a board that renders from it. Not "a file that looks like a tracker".
  • Build the smallest: one optional target the project implements, which prints the normalized rows in the contracts section below. It is twenty lines. The capability must never parse the project's own schema — that would carry one project's tracker into every future project forever. The shared side declares the contract; the project prints it.
  • Degrade: no target → the report shows no progress bars and states why, carrying the reason as a first-class value in the facts record (a stated absence, not an empty list). The reason reaches the page.

3. A durable per-period home, and a check the project already runs

Why it is load-bearing: the record must survive as the unit of history (principle 6), and the gates that keep the report honest must ride inside a command the project already runs. A new standalone gate needs every project to wire it up, and the project that forgets is exactly the one whose published history quietly stops matching its facts.

  • Probe: one command that the team believes means "green". A per-period directory convention anywhere in the docs tree.
  • Build the smallest: a directory per period holding the facts record and the documents; the existing check gains two lines.
  • Degrade: no check runner → the gates become a scheduled job that reports loudly. If they are not run at all, say so in the report's own honesty note: this report is ungated.

4. External access: a place to send the record, a credential, and a clock

Why it is load-bearing: the second reader is a machine outside your repository. Without a sink this capability is still worth building — but its ingestion half does not exist, and the recipe's exact contract is the part you skip.

  • Probe: does the fleet have a knowledge base that ingests documents, and does it publish a wire shape? If it does, that shape wins over the one below.
  • Build the smallest: an endpoint that accepts the envelope in the contracts section, upserts on (project, period), and requires a scoped write credential. The unattended run needs a scheduler on a machine that has the credential.
  • Degrade: no sink → the reports stay local and the aggregate is the whole deliverable; say in the human note that this project's history is not queryable by the fleet, so nobody assumes it is.

5. A presentation layer for the page

Why it is load-bearing: the page is forwarded — attached, mailed, opened on a phone with no access to your network. It must be one self-contained file with no external fetches and no scripting, or it will render as a broken skeleton in exactly the situation it was made for.

  • Probe: the project's own design tokens or shared stylesheet.
  • Build the smallest: one stylesheet, inlined at build time.
  • Degrade: no styling layer → an unstyled but self-contained page. Never a page that fetches its style at open time.
05

The contracts

The wire envelope

The one shape a machine outside the emitting repository parses: the fleet's shared knowledge base keys, dedups and indexes it. Copy it byte-for-byte.

Binding: exact

{
  "kind": "progress-report",
  "schema": 1,
  "project": "<project-slug>",
  "date": "<YYYY-MM-DD>",
  "title": "<human title of the period's document>",
  "source": "<sender-name>/<sender-version>",
  "reviewed": false,
  "content_format": "markdown",
  "content": "<the entire narrative document, its header included>",
  "metadata": null,
  "facts": null
}

Field notes — three of these carry traps:

field meaning, and the trap
kind routes future record kinds; the consumer branches on it rather than sniffing the body
schema the wire version. Version it on its own axis, never coupled to your local record's version — otherwise a local shape change silently misreports the wire contract
project + date the dedup key. Re-sending the same pair must replace, never duplicate. date is the period the report is about, not when it was sent (principle 9)
title untrusted free text (it is derived from the document) — it is serialized, never concatenated
source names the SENDER, not the generator. Both facts matter and the field carries one; carry the generating agent's version separately or a downstream reader will diagnose the wrong component (see the scars)
reviewed false = an unattended run produced it, nobody confirmed the scope; true = a human ran it and read the output. The consumer should surface this on any answer it derives — an unreviewed record is a weaker source
content the entire narrative, header included. This is the primary ingestible document
metadata declared nullable on purpose: the consumer requires the key and treats its interior as a loose index, because projects legitimately carry different keys. The shape it should hold is the adapt-bound block below
facts the structured record, verbatim, when the sink opts in. Also nullable: a consumer that reads a missing key as starvation and an explicit null as "not sent" gives the reader one unambiguous answer — emit the key always, null when facts are withheld or dropped to fit the size cap

Fixture: valid

{
  "kind": "progress-report",
  "schema": 1,
  "project": "acme-web",
  "date": "2026-07-16",
  "title": "acme-web — progress report, 2026-07-16",
  "source": "acme-reporter/1.4.2",
  "reviewed": false,
  "content_format": "markdown",
  "content": "---\nkind: progress-report\nproject: acme-web\n---\n\n# acme-web — progress report\n\nCheckout redesign reached its third phase.\n",
  "metadata": { "kind": "progress-report", "project": "acme-web", "date": "2026-07-16" },
  "facts": { "schema": 1, "project": "acme-web" }
}

Fixture: invalid — the sender renamed a key into its own language; the consumer's dedup starves

{
  "kind": "progress-report",
  "schema": 1,
  "slug": "acme-web",
  "date": "2026-07-16",
  "title": "acme-web — progress report, 2026-07-16",
  "source": "acme-reporter/1.4.2",
  "reviewed": false,
  "content_format": "markdown",
  "content": "# acme-web\n",
  "metadata": null,
  "facts": null
}

Fixture: invalid — the confirmation flag arrived as a string, so every record reads as truthy and "unreviewed" stops being distinguishable

{
  "kind": "progress-report",
  "schema": 1,
  "project": "acme-web",
  "date": "2026-07-16",
  "title": "acme-web — progress report, 2026-07-16",
  "source": "acme-reporter/1.4.2",
  "reviewed": "false",
  "content_format": "markdown",
  "content": "# acme-web\n",
  "metadata": null,
  "facts": null
}

Fixture: invalid — an extra top-level field is a private dialect the consumer never agreed to; it belongs in the loose index block

{
  "kind": "progress-report",
  "schema": 1,
  "project": "acme-web",
  "date": "2026-07-16",
  "title": "acme-web — progress report, 2026-07-16",
  "source": "acme-reporter/1.4.2",
  "reviewed": false,
  "content_format": "markdown",
  "content": "# acme-web\n",
  "metadata": null,
  "facts": null,
  "internal_run_id": "8f21"
}

The index block — the header the narrative carries and the envelope repeats

Derived wholly from the facts record and stamped atop the narrative, so a machine can classify the period without parsing the prose beneath it. It is the same block the envelope's metadata carries, pre-parsed, so the consumer need not read the document's header format.

Binding: adapt — the consumer requires the key, not the interior; different projects legitimately carry different measurements, and a record rejected over a schema quibble is a record that silently stops arriving.

kind: progress-report
schema: 1
project: <project-slug>
date: <YYYY-MM-DD>
window: { since: <RFC3339>, until: <RFC3339>, label: <human window name> }
author: <the distinct authors of the period's changes, derived>
reviewed: false
lang: <the language of the PROSE; the numbers have none>
repos: [<each repository this period covers>]
metrics:
  commits: 0
  code_commits: 0
  insertions: 0
  deletions: 0
  files: 0
  hours_derived: 0.0        # the PROXY (principle 3) — always present
  # the four below are emitted ONLY when a runtime was actually read.
  # absent key = not measured; a zero here means "measured, and it was zero".
  agent_hours: 0.0          # effort: concurrent spans SUMMED
  wall_clock_hours: 0.0     # elapsed: the same spans, overlaps MERGED
  tokens_input: 0           # fresh input only
  tokens_output: 0
epics: [{ slug: <work-item-slug>, percent: 0, phases: <done>/<total>, status: <lifecycle state> }]
tags: [<project>, progress-report, <work-item-slug>]
headline: <the frozen one-line judgment>

The trap is the four optional metrics: make them nullable/absent-capable in your types, or "not measured" and "measured as zero" collapse into the same output — which is principle 4 broken in the one half where nobody can see it.

The facts record — the durable local unit

Binding: adapt — nothing outside the repository parses it; the aggregate and the header are both derived from it, so its shape is yours to name as long as it carries these roles.

{
  "schema": 1,
  "project": "<project-slug>",
  "lang": "<language of the prose>",
  "date": "<the canonical period label — passed IN, never inferred from the window>",
  "window": { "since": "<RFC3339>", "until": "<RFC3339>", "label": "<human window name>" },
  "git": { "commits": 0, "code_commits": 0, "insertions": 0, "deletions": 0, "files": 0, "hours": 0.0, "authors": ["<name <address>>"] },
  "repos": [{ "name": "<repository role name>", "commits": 0, "insertions": 0, "deletions": 0 }],
  "epics": [{ "slug": "<work-item-slug>", "title": "<title>", "percent": 0, "phases_done": 0, "phases_total": 0, "status": "<lifecycle state>" }],
  "epics_source": "<how the rows were obtained, or 'none: <why>' — never silently empty>",
  "epics_warnings": ["<what the seam could not read; this reaches the page>"],
  "runtime": {
    "agent_hours": 0.0,
    "wall_clock_hours": 0.0,
    "sessions": 0,
    "tokens": { "input": 0, "output": 0, "cache_read": 0, "cache_write": 0, "processed": 0 },
    "providers": [{ "provider": "<runtime role>", "verified_build": "<the build its log shape was read against>", "sessions": 0, "agent_hours": 0.0 }],
    "absences": ["<a runtime with no readable log here, and why>"],
    "note": "<what these numbers do and do not measure>"
  },
  "method": {
    "session_gap_minutes": 0,
    "excluded_paths": ["<generated and vendored trees>"],
    "code_types": ["<which change types count as product work>"],
    "hours_note": "<the sentence the page must print about the proxy>",
    "runtime_note": "<the sentence the page must print about the measurement>"
  },
  "summary": {
    "headline": "<the frozen judgment>",
    "body": "<the frozen narrative summary>",
    "epics": [{ "slug": "<work-item-slug>", "line": "<what this item means, in the reader's language>", "estimate": "<a RANGE with its basis>" }]
  }
}

Two roles here are load-bearing and easy to mistake for bookkeeping. method is what makes principle 3 mechanical: the disclosure sentences travel with the numbers, so the page cannot be produced without them. summary is the frozen judgment written back into the record — because the aggregate is built from this file and never from the rendered page, a judgment that does not land here does not exist in the history.

The normalized work-item rows the project prints

Binding: adapt — this is the seam between the shared capability and the project's own tracker schema. The shared side declares it; the project's optional target emits it.

[
  { "slug": "<work-item-slug>", "title": "<title>", "phases_done": 0, "phases_total": 0, "status": "<lifecycle state>", "blocked_on": "<what it waits on, in plain language, or null>" }
]

blocked_on is not decoration: an unstarted item stays on the page with its reason. A row omitted because 0% "looks bad" is the one omission a reader cannot detect.

The reply the sender must classify

Binding: adapt — the sender logs the identifier and depends on nothing in it.

{ "id": "<stored document id>", "status": "stored" }

Classify by status class, not by parsing prose: 2xx = stored (both "created" and "replaced" are success — the sender must not care which); credential/malformed/too-large = fail loudly, no retry, because retrying a misconfiguration just repeats it; rate-limited or server-side = retry with backoff, then leave the period on disk and exit non-zero. A failed send never mutates local state — the period is on disk either way, and can be re-sent.

06

The build sequence

1. Build the collector — every number, derived, into the record

Take a window and the period's canonical label as separate inputs (principle 9), walk the version-control history, and write the facts record from the contracts section. Exclude generated and vendored trees from the line counts; derive effort by clustering change timestamps into sessions with a configurable gap; write the method sentences alongside. The collector is pure with respect to the repository: same window, same history, same bytes out.

Done when: running it twice over a closed window produces byte-identical records, and the record contains no field that a human typed.

2. Declare the work-item seam — and let it be absent

Declare the normalized rows as a contract and call an optional target the project implements. Never parse the project's own tracker. When the target is missing, unreadable, or returns nothing, record the reason as a value (none: <why>) and carry anything the seam could not read into a warnings list that reaches the page.

Done when: a project with no such target produces a report with no progress bars and a printed reason; a project with one produces rows; and a tracker that fails to parse produces a warning on the page rather than a silently shorter list.

3. Add the measured half — beside the proxy, never over it

If the agent runtimes keep session logs, read them through a declared surface — does this runtime persist a log, where, in what shape, which fields carry the working directory, the timestamp, the model, the token counts — stamped with the build you verified the shape against. Ask the surface, never the runtime's name. Normalize what the runtimes disagree about. Emit effort (summed) and elapsed time (merged) under separate names, and never overwrite the timestamp-derived proxy: the two sit side by side.

Done when: effort ≥ elapsed on a day with parallel sessions, elapsed can never exceed the window's length, and a machine with no logs produces a stated absence with its reason instead of a zero.

4. Write the judgment against the record, and freeze it back

The agent reads the facts and writes what a machine cannot: what the work means in the reader's language, what is left, and an estimate as a range with its basis ("two to three days: two dependency waves remain, the first of which is four parallel phases"). It types no quantity. Then the judgment is written back into the facts record.

Done when: every number in the prose appears in the record; the aggregate (step 7) can show the period's headline without ever opening the rendered page.

5. Render the page for people — self-contained, no scripting, escaped by construction

Inline the styling; no external fetches; no scripting. Order it so it reads in thirty seconds: a plain-words overview → the headline numbers, each under its own name → the work-item bars, including the ones at zero, each with its reason → what shipped, each item with why it matters → the rest, in small print → the tooling's own work, last (hiding it is dishonest; leading with it is self-absorbed). Print the method sentences as they are. Every untrusted string reaches the markup through a serializer or an escaper, never through concatenation.

Done when: the page opens correctly from a mail attachment on a machine with no network, and a change subject containing markup renders as visible text rather than executing or truncating the page.

6. Stamp the machine-readable header, and gate it

Emit the index block from the facts record — never from the history directly, so it can only say what the facts say — by marshalling a typed value, so an untrusted string round-trips as a quoted scalar instead of splitting the document. Add a check mode that re-derives the block and fails on drift. Tolerate a period with no block at all (a record written before the header existed is not drift), so the gate lands inert on history.

Done when: hand-editing one number in a stamped header makes the check fail, re-deriving makes it pass, and a period predating the header stays green.

7. Generate the aggregate — from the facts, never from the prose

One self-contained page across every period ever reported, built only from the facts records. It carries no clock — a "generated on" line makes its own drift gate fail on every run. Gate it for drift, comparing what the facts say, not the shared skin it inlines.

Done when: two consecutive builds with no new period produce identical substance; a centrally released style change does not turn any project's check red; and the aggregate's content demonstrably comes from the records (delete the rendered pages, rebuild, nothing is lost).

8. Build the sender — pure body, impure shell

Split it: a pure function that turns a persisted period into the exact envelope (facts + narrative in, bytes out — no clock, no network, so the shape can be held to a fixture), and a thin shell that moves the bytes. Assemble from a typed value and serialize. Read the credential at send time and put it in the transport header only. Send the structured record verbatim when the sink opts in — the same bytes on both sides. Classify the reply by status class per the contract. Keep a machine-local ledger of what was already sent so a re-run of an unchanged period is a no-op — as an optimisation on top of the consumer's own idempotency: a lost or corrupt ledger degrades to "send anyway", never to an error.

Done when: a dry run prints the entire payload with no credential anywhere in it; the payload passes a conform check against the exact contract; sending the same unchanged period twice produces one stored record and the second call is a no-op; and a forced re-send of a changed period replaces rather than duplicates.

9. Add the unattended run — one atomic verb for the scheduler

Collect → refuse to overwrite an immutable published period → hold back a period a human already confirmed (an unattended draft must never overwrite it, because latest-wins on the consumer's side would silently downgrade a confirmed record) → stamp it unconfirmed → rebuild the aggregate → send. It ships a draft; a human-run confirmation later supersedes it under the same key.

Done when: the scheduled run stamps the period it was asked for (not the window's edge), a confirmed period survives an unattended run untouched, and running it twice in one night stores one record.

10. Wire both gates into the command the project already runs

The drift gates ride inside the existing check, not in a gate of their own.

Done when: the project's own green command turns red on a seeded drift in either a stamped header or the aggregate — with no extra wiring in any project that already runs the check.

07

The seams

What must stay configurable, or the next project inherits values it never chose:

  • Where periods live — the reports home, through the project's structure seam.
  • The work-item schema — the project's alone. The shared side declares the normalized rows and consumes only those; a shared component that parses one project's tracker carries that project's schema to every project forever.
  • The method values — the session gap, the excluded paths, which change types count as product work. These are the levers people argue about; a number someone disagrees with is fixed here, never in the prose.
  • The audience and the language of the human document — while the machine-readable record stays in one fixed language, because its reader is a system reading the project's history later.
  • The sink — the endpoint's location, the name of the environment variable holding the credential (never the credential), whether the structured record rides along, and whether this project sends at all. Per project: each project is its own document stream on the consumer's side.
  • The period's coverage — the repositories a period folds in, standing plus a per-period opt-in.
  • What "reviewed" means operationally — who is allowed to stamp confirmation.
08

The scars

Ticked boxes reported the exact opposite of the truth

Symptom: a report generated from ticked acceptance boxes showed 0% on a work item that was more than a third complete: 0 of 41 boxes ticked across its documents, while 7 of its 17 tracked phases were done. Root cause: the boxes had never been the artifact anyone maintained. Nothing consumed them, so nothing kept them true — and a report is the first consumer prose has ever had. Fix: progress is read from the work items' machine-readable state or it is not read at all; where no such state exists, the report shows no progress and prints why (principle 2, ground 2).

Summing parallel sessions produced 25.3 hours inside a 24-hour day

Symptom: the effort figure exceeded the length of the day it described. Root cause: agent sessions run concurrently — subagents, worktrees, two terminals — so the sum of their active spans is machine effort, which is true, and impossible as elapsed time, which is what a reader assumes a figure in hours means. Fix: two numbers, each under its own name: effort is summed, elapsed time is the union of the intervals and can never exceed the window. The gap between them is the parallelism, which is information, not an error to smooth away.

The headline number was 1.85 billion, and 98% of it was the same context re-read

Symptom: a period "processed" 1.85 billion tokens — a spectacular figure, and meaningless. Root cause: cache re-reads dominate the total: the same context re-served turn after turn. A total handled is not a measurement of work done. Worse, two runtimes counted their cached tokens on opposite sides of the input line (one inside it, one beside it), so a single naive input column across both would have been a number nobody could defend. Fix: print fresh input and output only; name everything else separately; normalize the providers' conventions on a declared flag and record which convention each source used.

The period was stamped a day late, under the wrong dedup key

Symptom: the unattended run asked for a given day and produced a record labelled the day after; asked for yesterday, it stamped today. Found on the first end-to-end send against the live endpoint — every gate was green. Root cause: the run's windows are half-open — [day 00:00, next-day 00:00) — so the upper bound is the next day's midnight, and the label was derived from it. A window alone cannot name its period when its upper bound is exclusive. Because the consumer dedups on (project, period), the record would have landed under the wrong key — the one failure that cannot be corrected by re-sending. Fix: the canonical period label is an explicit input to the collector (the caller already computes it for the directory), falling back to the window's edge only in the convention where that edge sits inside the target period. Teeth: an end-to-end test asserts the stamped label equals the day requested.

A change subject with a colon split the machine-readable header

Symptom: a header block, hand-assembled from derived strings, broke its own parser; the same class of string reaching the page could inject markup. Root cause: a derived string is untrusted regardless of how trustworthy its source is — a subject line carrying a colon, a leading dash, or markup is free text the moment it enters a structured document. Escaping-by-remembering fails at exactly one call site, eventually. Fix: the block, the page and the wire body are all built from typed values handed to a serializer — never concatenated. The escape then cannot be forgotten, because nobody performs it. This one hazard belongs to the genre: anything that renders text it did not write, into an artifact that gets forwarded, inherits it.

The collector blamed the data for a broken toolchain

Symptom: on the first live run against a real project, every work item came back "unreadable" — in a report whose entire purpose is telling the truth about the data. Root cause: the task runner and the operator's shell resolved different interpreters, only one of which had the parsing library installed. The script worked by hand and died under the runner; the caller read the failure as "this tracker is invalid". A broken toolchain wore a data failure's clothes. Fix: the toolchain a target runs on is declared, never ambient — resolved in a stated order, and when it cannot be, the failure is loud, carries the fix, and is distinguishable from a data failure. A collector that reports "no data" when it means "I am broken" produces a report that merely looks quiet.

A downstream team diagnosed a regression that did not exist

Symptom: the knowledge-base team read a record stamped with a recent sender version, saw a near-empty index block, and correctly reasoned their way to "the index serialization regressed in that version". It had not. The record had simply been built by an older agent, before the header existed, and sent by a new one. Root cause: the field named the sender while the reader took it for the generator, and nothing anywhere recorded the generator at all. A real consumer misdiagnosed a real defect out of one field's ambiguity — which is the strongest possible evidence that a name is wrong. Fix: carry both facts, each named for what it is. Any field that could mean "who made this" or "who moved this" must say which, and the other must exist somewhere.

Attribution by directory name silently dropped whole sessions

Symptom: work done in a subdirectory of the project vanished from the measured half. Root cause: the runtime files a session under a slug of the directory it started in — a session started one level down gets its own slug. Matching the project by that slug is matching on a lossy key. Fix: both runtimes record the working directory inside the log; attribution reads that and asks whether the path is inside the project. Where a period spans repositories, the sessions are deduplicated: a session must count once even when it sits under two roots.

A byte-for-byte drift gate reddened projects that had changed nothing

Symptom: the aggregate's drift check would fail in every project the day a central design release touched a token — for a difference the project neither made nor could re-derive. Root cause: the aggregate inlines the shared skin (that is what keeps it one shareable file), so a byte comparison compares two things at once: the substance the project owns and the presentation somebody else releases. Fix: gate the substance, not the skin — the comparison covers what the facts say, and the presentation refreshes on the next build. The cost is stated, not discovered: an aggregate wears the previous release's skin until the next period is generated.

09

Verification

The gates that must ship with the capability — an unguarded invariant rots back to nothing, and every one of these guards a hand-check somebody would otherwise do once:

  • Header drift — re-derive the index block from the facts and fail on any difference. Teeth: hand-edit one number, the check must fail. It must also stay green on a period that predates the block.
  • Aggregate drift — rebuild and compare the substance. Teeth: hand-edit the aggregate's content, the check must fail; release a style change centrally, the check must stay green.
  • Immutability — the collector refuses to overwrite a published period. Teeth: re-run over a published period and observe the refusal, not a rewrite.
  • The wire body against the exact contract — the pure payload builder is held to a fixture, and the project's own check conforms a real payload against the shape shipped in this document. Teeth: rename one field, the check must fail. This is what turns "everyone sends the same shape" from an agreement into a gate.
  • Absence is not zero — a fixture with no readable runtime log must emit a stated absence. Teeth: a test asserting that "measured, and it was zero" and "not measured" produce different output.
  • Surface freshness — every declared runtime-log surface carries the build it was verified against, and a check fails when the installed build has moved past it. A log format rots exactly as silently as a hook does: a provider renames one field, the report keeps printing a number, and nothing says a word. (In the verifying build this gate caught a stale stamp within an hour of being widened to cover every stamped surface.)
  • Both drift gates ride inside the command the project already runs. A gate registered where the project does not look is a gate that passes forever.

The one live check no gate performs: generate a real period, open the page from a mail attachment on a machine with no network, and send it — twice. Then read it back from the consumer. The day-late stamp, the empty index block, and the "unreadable" trackers were all found this way, by looking at the first real output, after everything was green.

10

The trade-offs

  • You now own a schema and a collector. The facts record is a durable shape with a version and history behind it; every change to it is a migration question. This is the price of principle 1, and it is not small.
  • Freezing a period makes mistakes permanent. A published record someone read cannot be quietly improved — that is the point, and it will hurt the first time a typo ships.
  • The judgment is not free. A machine derives the numbers; a person or an agent still has to say what they mean, in the reader's language, every period. If nobody is willing to spend that, you will produce a page of numbers nobody reads.
  • The seam costs the project something. Progress requires the project to publish its work items' state. Projects that will not do it get an honest report with no progress in it — a supportable outcome, and a disappointing one.
  • An exact wire contract binds the fleet. Changing a field is a breaking change for everything that parses it; the flexibility lives in the loose index block, on purpose.
  • The aggregate wears last release's skin until the next period is generated — the stated cost of gating substance over bytes.

When not to build this at all: when nothing recurs (a one-off answer to "how's it going?" is a sentence, not a pipeline); when the reader can check the claim themselves (a changelog is the record, and the changes are readable); when the artifact is an internal orientation note for people who can open the repository; and when nobody outside the work is asking — the machinery earns its place only when a report recurs, because recurring is what makes someone start trusting it.

11

For the human

You get a pipeline that makes a daily report impossible to fake by accident. A collector reads your history and writes every number into one durable record per day, with the method behind each number sitting beside it. An agent then writes the only part a machine cannot — what the work means and what is honestly left — against that record, and cannot type a number without it being visibly absent from the facts. Out of the same record come three things: a page a partner can open on a phone with no network, a Markdown document a machine can read, and one idempotent transmission into the knowledge base your fleet queries later. Re-sending a day changes nothing; a day someone read on Monday is the same document on Friday; and a day nothing could measure says so, out loud, instead of showing a confident zero.

The stack it was built on: a single statically-linked binary (Go) doing every derivation, driven by a coding agent that writes only the judgment; the version-control log as the source of truth; JSON for the durable record and the wire body, YAML for the header a machine reads off the top of the document; the page as one self-contained HTML file with zero scripting.

Essential — replace these and the capability degrades into the thing it was built to replace:

  • A compiled, dependency-free collector. The numbers must come from something with no imagination, and it must run identically on a laptop, in a scheduled job, and inside a check. The moment deriving a number requires a service to be up, somebody will type the number.
  • A durable machine-readable record as the single axis. The page, the header, the aggregate and the wire body are all projections of it. This is the choice everything else rests on.
  • Serialization from typed values. Escaping is achieved by construction, never remembered.
  • A self-contained, zero-scripting page. The artifact gets forwarded; anything it must fetch at open time will be missing exactly when it matters.
  • Checks that run on one command with no services up — otherwise the gates get skipped, and ungated invariants rot.

Incidental — the author's house, swap without a second thought: Go itself (any language that compiles to one runnable artifact does this); YAML for the header (JSON reads fine, it is just denser on top of a document people also read); the particular directory convention per period; the house stylesheet; the specific verb names.

What that stack bought, and what it cost. It bought distribution: one artifact, no runtime to install, so the same collector runs in a scheduled job and in a check, and the "it works on my machine" class of report failure never appeared. It bought a compiler that makes the "measured zero vs not measured" distinction a type, not a convention. It cost ceremony: a shape change is a code change, a release, and a version bump — no quick reshaping of the record over lunch, which is a real tax when the shape is still moving. It cost a second language in the loop: the agent that writes the judgment does not write the collector, so the split has to be maintained deliberately rather than emerging. And it cost the rebuild-before-you-benefit step for every consumer of the binary: an improvement to a number's method reaches a project only when that project reinstalls, which is a real coordination cost the day the fix is urgent.

the handover protocolappended on export

These two sections are the same in every recipe — `sporo export` appends them so the reader knows how to bring the capability into a repository the author never saw, and what to send back.

Adopt it here

This section is addressed to the agent that is reading this recipe inside its own repository. The recipe above knows the capability; only you know the ground. Do this before the first build step.

Probe, do not assume. For each thing the ground section says must be standing, go and look for it here. Do not reason about what a project like this "usually has" — read the repository. Then say, in one line each, what you found and what you did not.

Agree the outcome before you build. The recipe says what the capability can produce; only your operator knows what it must produce here. Ask them, before the first build step: which of the outputs are needed in this project, in what form, and which outside consumers will read them. Do not assume every output transfers — a capability that emits three artifacts at home may owe this project only one. And treat the bindings as law: a shape marked **Binding: adapt** is yours to rename into this project's language; a shape marked **Binding: exact** is not yours at all — something on the other side parses it, so confirm with your operator that the consumer applies here, then copy it byte-for-byte — and check your output against it mechanically, in this project's own CI, from this document alone (the recipe tool's conform check does exactly that; if you cannot run it, validate against the shape by hand and say so in your report).

Map the roles onto this repository's homes. The recipe names roles — the facts file, the collector, the optional target the project implements — because a path is the one thing that cannot travel. You own the other half: decide where each role lives here, in this project's own conventions, and write the mapping down where the next agent will find it (the project's configuration, not your context window). A mapping that exists only in the session that made it is a mapping that will be re-invented, differently, next week.

What is missing, propose — do not silently substitute. When a precondition is genuinely absent, you have exactly three honest moves, in this order:

  1. Create it, at the smallest size that works. The recipe's ground section says what shape it needs and why it is load-bearing; carry that reason across in the language of this project, not as an appeal to someone else's conventions. A precondition worth a ladder in the recipe is usually worth twenty lines here.
  2. Degrade, and label it in the output. If the recipe states a fallback, take it — and carry its label all the way into what the capability produces. A degraded source that is not labelled where the reader can see it is indistinguishable from the real thing, which is the failure the recipe was written to prevent.
  3. Stop and ask. If neither is available, say so before you build. A capability standing on a precondition that is not there produces confident output over rot.

There is no fourth move, and in particular: do not quietly reach for a weaker source that happens to be lying around. That substitution is how nearly every scar in every recipe began.

Do not import the author's structure. What transfers is the principles, the contracts, and the scars. The directory layout, the file names, the command names and the tool are the author's house, and copying them buys you nothing and costs you a shape that does not fit.

State the plan before you build. The mapping, what you will create, what you will degrade and how it will be labelled. That statement is the reader's only chance to correct you while it is still cheap.

Report back

A recipe improves in exactly one way: someone builds it somewhere else and says what happened. Every scar in the document above is a debt paid by an earlier reader. When you finish, send this back to whoever handed you the recipe. Open with the recipe's name and the version: from its frontmatter — a report that cannot say which text it built floats free of every fix it triggers.

  • Stack — what you actually built it on, and which of the author's essential choices you kept, replaced, or could not honour.
  • Degraded — every place you took a weaker path than the recipe asked for, what forced it, and the label it now carries in the output.
  • New scars — the payload, and the reason this section exists. Anything that broke, that the recipe did not warn you about, as symptom → root cause → fix. A scar you hit is a scar the next reader does not have to.
  • Wrong — anything the recipe asserts that turned out not to be true on your ground. Being contradicted by a build is the only way a recipe finds out it is stale.
  • Arithmetic — the recipe names one live check that no gate can perform. Say whether you ran it, and what it said.
  • Missing — the thing you had to invent because the recipe described it in prose and did not hand you the shape. If you had to design a contract, send it: it belongs above.

Keep it short. Five honest lines beat a report nobody writes.