0014 — Agent Provenance: One Identity Across Inbox, Memory, and Git¶
Status: Accepted — SA-owned. The enforcement half (commit-msg hook + CI check) is built and live on main — #161: scripts/agent_trailer.py (inject + validate) and the agent-provenance.yml CI gate, spawn-aware, with an Agent: human bypass. Live proof: PR #163 (766b7a0) carried Agent: oe and passed the gate. Residual #162 (install the hook in each local clone) is low-priority — CI enforces server-side regardless.
Decided by: User (direction — extend provenance to git as a sibling of memory's C5, 2026-06-15).
Author / Sign-off: System Architect.
Spun out of: ADR-0013 Decision (c) — separated because it is independently landable and crosses into the Infrastructure Perimeter.
Relates to: the team-memory contract clause C5 (last-edited-by provenance); ADR-0010 §2 (worktree-per-agent); ADR-0012 (converge, lead-and-spawn execution).
Context¶
A klartext colleague has one identity that is rendered across several substrates, but until now git was the gap. The identity facts:
- SSOT = the
agents/<name>/directory name (the slug).team.yamlcarries metadata only — it is not the source of identity. - Already rendered: the inbox
from/tofields; team-memory last-edited-by (contract clause C5). - Missing: git. All agents commit under the single GitHub identity
ThorstenDittmar(the reason GitHub rejects self-approval on our PRs — see the SA sign-off process). A commit therefore does not record which agent authored it. With lead-and-spawn execution (ADR-0012 / ADR-0013), where a lead spawns sub-agents that commit, the attribution gap widens.
This is the same provenance need C5 answers for memory, for a different substrate. The trailer is git's sibling of C5 — a parallel mechanism, not a generalisation of it: C5 self-stamps memory files (which live outside git), the trailer stamps git commits. Two provenance mechanisms for two substrates; neither subsumes the other.
Decision¶
Record agent provenance as a git commit trailer.
- Trailer:
Agent: <slug>— where<slug>is theagents/<name>/directory name (the SSOT). - Spawn-aware: when a lead has spawned a sub-agent to do the work, the trailer is
Agent: <lead> (spawned <task>)— the accountable identity is the lead; the spawn is visible. - Placement: a standard git trailer in the commit message footer, alongside the existing mandated
Co-Authored-By:line. It is additive — it does not replace the Co-Authored-By convention. - Enforcement (Enforcement Hierarchy — mechanical):
- a commit-msg hook that adds/validates the
Agent:trailer at commit time, and - a CI check that rejects a method/agent commit lacking a well-formed trailer. This is the most strongly enforceable rendering of the one-identity model — git history becomes self-describing.
- Deferred: per-agent real GitHub identities (distinct authors/accounts). Out of scope here; start with the visible, enforceable trailer. Revisit only if the single-identity constraint (self-approval, attribution in the GitHub UI) becomes a binding problem.
Rejected / deferred alternatives¶
| Alternative | Verdict | Reason |
|---|---|---|
| Per-agent GitHub identities now | Deferred | Heavier (accounts, auth, key management) and not required to close the attribution gap. The trailer gives self-describing history immediately; real identities can follow if needed. |
Derive the agent from team.yaml |
Rejected | team.yaml is metadata, not the SSOT. Identity is the agents/<name>/ directory name; the trailer must reference that, so it cannot drift from team.yaml. |
| Bundle into ADR-0013 | Rejected | Separable and Infrastructure-Perimeter; independently landable (this ADR). |
Consequences¶
Positive - Git history records which agent produced each commit → the one-identity model is complete across inbox, memory, and git. - Spawn-aware form keeps accountability with the lead while making the spawn visible — fits the lead-and-spawn execution model of ADR-0012/0013. - Mechanical enforcement (hook + CI) — not a review-time convention that erodes under pressure.
Negative / Risks - A new commit-msg hook is friction if it rejects commits; it must fail with a clear, copy-pasteable fix and a documented bypass for genuine non-agent commits. - Until the hook lands, the trailer is a manual convention (ritual-level) and applied unevenly. Mitigated — the hook + CI gate landed in #161; the trailer is now mechanically enforced.
DevOps Briefing¶
Need: A commit-msg hook + CI check enforcing the `Agent: <slug>` commit trailer
(spawn-aware: `Agent: <lead> (spawned <task>)`), slug = agents/<name>/ dir name.
Why: Close the git attribution gap — git is the one substrate where the single
GitHub identity (ThorstenDittmar) hides which agent authored a commit.
Git's commit-trailer sibling of team-memory contract clause C5 — a parallel
provenance mechanism for a different substrate, not a generalisation (ADR-0014).
Domain: Config / CI / Hooks (Infrastructure Perimeter: .pre-commit-config.yaml or a
commit-msg hook, .github/workflows/, possibly api/cli.py for a helper).
Approach: (suggestion) commit-msg hook injects/validates the trailer from the worktree's
agent slug; CI rejects a method/agent commit without a well-formed trailer;
define the non-agent bypass and the spawn-aware form.
Impact: All agents — every commit gains the trailer; CI gate applies to method/agent paths.
SA defines the rule (this ADR); DevOps owns the mechanism and wires the hook + CI. Neither acts alone — the rule is documentation until DevOps enforces it.
Open questions — resolved by the enforcement build (#161)¶
- ~~Exact scope of the CI gate — all commits, or only method/agent-path commits?~~ → All commits.
agent-provenance.ymlvalidates every commit on the branch, with an explicitAgent: humanbypass for genuine non-agent commits — not a path-restricted gate. - ~~The non-agent bypass form — explicit marker vs allowlist.~~ → Explicit marker:
Agent: human(no allowlist). - ~~Helper in
api/cli.py(klartext commitwrapper) or purely in the hook?~~ → Purely in the hook — the logic lives inscripts/agent_trailer.py; no CLI wrapper.
Known limitation (parked, 2026-06-17): the trailer stamps the agent, not the generation — a predecessor and a successor of the same role both read Agent: <slug>. Surfaced when #163 was squashed (two Agent: oe stamps collapsed to one). Tracked in team-memory project_adr0014_generation_provenance; currently judged unimportant.