One of my early passes refused to close while every receipt was green.

The agents had done their work, every integration had its validation, and the final run passed. Closure still said no, because one commit on the branch had no explanation. I had made it myself: a reviewed fix, committed directly to the base branch while the pass was running. The harness had no way to know it was legitimate, and at the time, no way to be told.

In another pass, the opposite happened. Two processes updated the pass’s records at the same moment, and an integration that had been recorded was silently lost. The commit was on the branch. The record saying where it came from was not.

Both cases have the same shape. Validation says “this state passed”. It does not say “and this is everything that happened”. The second claim needs its own accounting.

This is the second of three articles on the harness I built to let coding agents work unattended. The first bound a test result to a commit; this one binds every commit on the branch to an origin. The code is written for the article and stands on its own.

Exactly once, in both directions

The rule: take the range a pass produced, from the base SHA it froze when it started to the head its final receipt proves. Every commit in that range has exactly one account.

Zero accounts means work landed that nobody is accountable for. That is the obvious half.

Two accounts is the half people miss. If a commit can be explained twice, the accounting is no longer one-to-one, and the total stops meaning anything. Eleven commits and eleven accounts look complete. But if one commit is claimed by both an integration and an operator’s note, eleven accounts cover ten commits, and the eleventh, the one nobody explained, hides behind the duplicate. Count the accounts per commit, never in total.

The ledger has a second side. Every commit an agent made must be either integrated or superseded by something that was. Otherwise work can be dropped halfway through an integration, and nobody notices, because the branch looks clean.

Why cherry-pick, not merge

An account needs a recorded origin, and a cherry-pick gives you one at the moment it happens: this agent commit became that commit on the base branch, and here is the receipt proving the branch still passed afterwards.

A merge does not. After a merge, “which agent wrote this line” becomes a question about history topology rather than a fact somebody wrote down, and the rule above becomes something you argue about.

Integration happens one commit at a time, oldest first, each followed by its own validation. When the branch breaks, you know which import broke it, not merely that something in a batch of ten did.

The price is real. Integration is slower than a merge, and every branch involved must keep a linear history, with no merge commit anywhere. Here it is worth paying, because the alternative is a ledger you cannot close.

src/provenance/accounts.ts
export type Integration = {
  kind: "integration";
  source: string; // the agent's commit, on its own branch
  target: string; // its cherry-pick on the base branch
  receiptPath: string; // the receipt proving the branch after the import
};

export type External = {
  kind: "external";
  target: string; // a commit the operator made on the base branch
  reason: string;
};

export type Account = Integration | External;

export type Supersession = {
  superseded: string[]; // agent commits never integrated one by one
  replacement: string; // the corrective commit that carries them, integrated
};

The closure check

The heart of it is one function, run when the pass wants to close, that returns the reasons it may not.

It works in three steps. Every account must target a commit of the range, and an integration must carry a receipt that proves the branch at its target. Every commit of the range must be counted exactly once. Every agent commit must be mapped or superseded.

src/provenance/close-pass.ts
import type { Account, Supersession } from "./accounts";

const short = (sha: string) => sha.slice(0, 8);

export function closureErrors(input: {
  range: string[]; // base..head on the base branch, oldest first
  accounts: Account[];
  agentCommits: string[]; // every commit the agents made
  supersessions: Supersession[];
  receiptProves: (receiptPath: string, target: string) => boolean;
}): string[] {
  const errors: string[] = [];
  const inRange = new Set(input.range);
  const counts = new Map<string, number>();

  for (const account of input.accounts) {
    if (!inRange.has(account.target)) {
      errors.push(`${short(account.target)} is not in the pass's range`);
    }
    counts.set(account.target, (counts.get(account.target) ?? 0) + 1);
    if (
      account.kind === "integration" &&
      !input.receiptProves(account.receiptPath, account.target)
    ) {
      errors.push(`${short(account.target)}: its receipt does not prove the branch after it`);
    }
  }

  for (const commit of input.range) {
    const count = counts.get(commit) ?? 0;
    if (count === 0) errors.push(`${short(commit)} landed on the branch and nothing explains it`);
    if (count > 1) errors.push(`${short(commit)} is explained ${count} times`);
  }

  const mapped = new Set(
    input.accounts.flatMap((account) => (account.kind === "integration" ? [account.source] : [])),
  );
  const superseded = new Set(input.supersessions.flatMap((entry) => entry.superseded));
  for (const commit of input.agentCommits) {
    if (!mapped.has(commit) && !superseded.has(commit)) {
      errors.push(`${short(commit)} was abandoned mid-integration`);
    }
  }

  return errors;
}

The messages are the interface. A commit made on the branch during a pass, by a person or by anything else, ends up as a line like this one:

text
7c41d09b landed on the branch and nothing explains it

That line does not say something is wrong somewhere. It names the commit and the missing piece.

Proving a supersession instead of declaring it

Sometimes an agent’s last commits are never integrated one by one: they break the branch, and a corrective commit replaces them. “Those were superseded” is then a claim like any other, and claims are what this design refuses to take on trust.

The proof is a patch id. git patch-id --stable hashes a diff while ignoring line numbers and commit metadata, so two diffs of the same change hash alike, even on a base that moved. Take the change the superseded series made, as a whole, and the change the replacement makes. If their patch ids are equal, the replacement carries the same work.

src/provenance/patch-id.ts
import { execFileSync } from "node:child_process";

const git = (args: string[], input?: string) =>
  execFileSync("git", args, { encoding: "utf8", ...(input === undefined ? {} : { input }) });

// The patch id of a diff: stable across moved lines and commit metadata.
export const patchIdOf = (diff: string): string =>
  git(["patch-id", "--stable"], diff).split(" ")[0] ?? "";

// A corrective commit supersedes a series only if it carries the same change
// as the series taken together.
export function provesSupersession(
  seriesBase: string,
  seriesTip: string,
  replacement: string,
): boolean {
  const series = patchIdOf(git(["diff", seriesBase, seriesTip]));
  const recovery = patchIdOf(git(["show", "--format=", replacement]));
  return series !== "" && series === recovery;
}

Two shapes are allowed. Either the tail of a series is superseded after a prefix that was integrated normally, or the whole series is, and the whole series only when nothing of it was integrated. Otherwise a supersession could quietly erase an integration that did happen.

The same proof backs a plain cherry-pick, and it has one blind spot. A change re-applied on a branch that moved can differ from its source only by its context lines, because a concurrent commit edited a neighbouring line. The exact patch ids then differ, and rightly so: nothing proves the two are the same change. In my setup, only the operator may map it, with a reason, after comparing the patch ids computed without context.

An escape hatch the agents cannot use

People commit to the base branch during a pass: a reviewed fix, a dependency pin, a rule every repository needs today. My harness refused that at first. Closure blocked, and the only way out was rewriting history, which is exactly the habit the ledger exists to kill.

Forbidding it only teaches people to work around the tool. Recording it, with a reason, keeps the ledger true.

src/provenance/record-external.ts
import type { External } from "./accounts";

export function recordExternal(
  sha: string,
  reason: string,
  context: {
    isTerminal: boolean;
    range: Set<string>;
    accounted: Set<string>;
    meetsCommitPolicy: (sha: string) => boolean;
  },
): External {
  if (!context.isTerminal) throw new Error("the operator preserves a commit, from a terminal");
  if (!context.range.has(sha))
    throw new Error("only a commit of the pass's range can be preserved");
  if (context.accounted.has(sha)) throw new Error("this commit is already accounted for");
  if (!context.meetsCommitPolicy(sha))
    throw new Error("an external commit follows the commit policy");
  if (reason.trim() === "") throw new Error("say why it is preserved");
  return { kind: "external", target: sha, reason };
}

Each guard closes one way to abuse the hatch:

  • From a terminal only, so an agent cannot preserve its own stray commit. In my setup, the agents are told to report a blocked closure instead.
  • Inside the range, so nothing from before the pass can be adopted after the fact.
  • Not already accounted, so the hatch cannot create a double count.
  • Under the same commit policy as agent work: signature, sign-off, linear history.
  • With a reason, which lands in the closure record.

“External” means not produced by an agent. It never means exempt from the rules.

What it costs, and when not to bother

The bookkeeping is unforgiving. Reword or amend a commit after it was integrated, and its recorded target no longer exists: closure refuses until the mapping is repaired.

So a repair path has to exist. A lost mapping can be recorded again with its original receipt, as long as that receipt proves an ancestor of the current head. That allowance exists because of the lost mapping from the opening: by the time I noticed it, the branch had moved on, and a receipt that had to prove the current head made the repair impossible. What no repair may do is make a commit disappear from the accounting. The only outcomes are “explained” and “closure fails”.

Serial cherry-picks, each followed by a validation, are slower than one merge.

For one developer and no agents, git log and code review do this job well enough. The ledger earns its keep when several writers you did not watch share one branch.

And like receipts, it defends against mistakes and drift, not against a writer with full access forging its own records. The privilege boundary stays where it was: signed commits, protected branches, review.

Nothing on the branch without a name

If several agents write to one branch, check your integration against this list:

  • Integrate by cherry-pick, one commit at a time, each with its own receipt.
  • Record the pair (source, target) when the cherry-pick happens, not afterwards.
  • Close only when every commit of the range has exactly one account.
  • Prove a supersession with a patch id; never accept it as a claim.
  • Let the operator preserve a direct commit, from a terminal, with a reason.
  • Keep the branches, the receipts and the failed ones after closure.

Validation tells you a state is good. Provenance tells you it is the whole story. Close nothing until both hold.

Mohammed Hamdoune is a Staff Software Engineer specializing in software architecture and AI-assisted delivery. He writes about real-world engineering decisions at sidlynx.me and founded LRJI, a software architecture consulting firm at lrji.co.