repo-manager/workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md
tegwick 35e86d7b85 feat: advance repository records and provenance
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
2026-08-21 22:07:48 +02:00

288 lines
12 KiB
Markdown

---
id: RMGR-WP-0009
type: workplan
title: "Coding-assistant commit provenance trailers"
domain: infotech
repo: repo-manager
status: active
owner: codex
topic_slug: infotech
created: "2026-08-21"
updated: "2026-08-21"
parent_project: prj-state-hub-retirement
related:
- RMGR-WP-0004
- RMGR-WP-0005
- ADR-007
- ADR-010
state_hub_workstream_id: "0516ce7b-c0fe-4555-bc31-eb7943c163ef"
---
# Coding-assistant commit provenance trailers
## Goal
Make commits written by coding assistants self-identifying — which assistant,
which model, which process — so that "who changed this and did two sessions
collide?" is answerable from `git log` alone.
## The gap
Observed 2026-08-21 while auditing `state-hub`. The repo has exactly two commit
identities:
```text
tegwick <bernd.worsch@gmail.com> 120 of last 200
custodian-sync <custodian-sync@railiance.local> 80 of last 200
```
Claude Code commits are identifiable only because they carry a
`Co-Authored-By: Claude Opus 5` trailer, added by a prompt instruction in
`~/.claude/CLAUDE.md`. Codex and Grok add nothing. So a commit made by Codex is
byte-identical in provenance to one typed by hand, and the question "which
assistant made this change" has no answer.
This is not hypothetical. On 2026-08-21 a Codex session ran for ~14 hours on the
workstation while a Claude session worked the same repo. Establishing that they
had *not* collided required correlating process tables against commit timestamps
and a trailer only one of them writes — and the conclusion could only be stated
as "not me and not the sync bot", never as "this was Codex".
It also weakens the multi-writer work already underway. `ADR-007` decision 2 and
`ADR-010` are about several writers over one artifact set; per-writer provenance
is the evidence those decisions need and currently lack.
## Category boundary — read before implementing
Three distinct kinds of non-human commit exist or are coming. **Only the first
is in scope here.** The scheme must not foreclose the others.
| Category | Identity model | Status |
| --- | --- | --- |
| **Coding assistant** | Runs under Bernd's account; author stays `tegwick`. Interactive, returns to the operator for instruction. | **This workplan** |
| **Kaizen agent** | Autonomous; will have its own agent id and account, and should commit as itself. | Future, not here |
| **Scheduled / OpenRouter capacity** | Unattended runs against purchased inference capacity. | Separate category, not here |
**The author is deliberately not changed.** Coding assistants commit as Bernd
because he is supervising, and that placement of responsibility is the point —
it is not an accident to be corrected. These trailers add *provenance*, not a
second actor. A future kaizen agent committing under its own identity is a
different mechanism and a different decision.
## Define the trailer contract
```task
id: RMGR-WP-0009-T01
status: done
priority: high
state_hub_task_id: "39ac4f86-a936-4726-8ad2-d6219f24b1df"
```
Publish the trailer set as a repository standard in
`docs/repository-standards_v0.1.md` — repo-manager owns repository standards
under decision `747011c6`.
Requested core fields are **model and process**. Proposed shape, using git
trailers so `git interpret-trailers` and `git log --format=%(trailers)` parse
them without custom tooling:
```text
Assistant: claude-code
Assistant-Model: claude-opus-5
Assistant-Process: 208772@bnt-lap001
Assistant-Session: 354884ba-6e26-4918-8bcc-4fc675e419ee
```
Two fields beyond the request, both cheap and both load-bearing for the stated
purpose — **cut them if they are not wanted**, the scheme works without:
- **`@<host>` on the process.** A bare PID is ambiguous across the fleet;
`bnt-lap001`, `railiance01`, and `coulombcore` all allocate from the same small
number space. Without the host, two unrelated sessions can read as one.
- **`Assistant-Session`.** PIDs are recycled by the OS and mean nothing once the
process exits, so a PID alone cannot group a session's commits after the fact
— which is exactly the collision question. Claude Code already exports
`CLAUDE_CODE_SESSION_ID`; if Codex or Grok expose no equivalent, omit the field
for that tool rather than inventing one.
Decide also whether `Co-Authored-By: Claude Opus 5` is kept, folded into
`Assistant-Model`, or dropped. Keeping both duplicates the model.
Result (2026-08-21): the contract is published in
`docs/repository-standards_v0.1.md`. `Assistant` is required when detected;
model, host-qualified process, and session are conditional on availability.
`Assistant-Model` is canonical and assistant-specific `Co-Authored-By`
conventions are deprecated.
## Implement the hook
```task
id: RMGR-WP-0009-T02
status: done
priority: high
state_hub_task_id: "7aaccdde-b79f-43ca-b1cd-81c7c3153229"
```
A `prepare-commit-msg` hook that appends the trailers from environment, wired
fleet-wide via `core.hooksPath` so it applies without per-repo installation.
There are no git hooks anywhere today — `core.hooksPath` is unset globally and
locally, and no repo has a non-sample hook — so this is greenfield and cannot
conflict with existing tooling.
Requirements:
- **Never block a commit.** Missing or unreadable environment means write no
trailer, not fail. A provenance mechanism that stops work will be disabled.
- **Idempotent.** Appending must not double a trailer already present, so
`--amend` and rebases stay clean.
- **Silent for humans.** A commit typed by Bernd with no assistant environment
set gets no trailers and looks exactly as it does today.
- Detection is from environment, not process-tree walking — the hook runs
several levels below the agent and the tree is not reliable.
Prefer `core.hooksPath` over per-repo `.git/hooks` (not committable, lost on
clone) and over committed hook files (needs opt-in per repo).
Result (2026-08-21): `.githooks/prepare-commit-msg` is installed globally via
`core.hooksPath=/home/worsch/repo-manager/.githooks`. Tests prove human silence,
exact-token idempotency, and non-blocking behavior. Tool-neutral overrides are
`ASSISTANT_NAME`, `ASSISTANT_MODEL`, `ASSISTANT_PROCESS`, and
`ASSISTANT_SESSION`.
## Wire Claude Code
```task
id: RMGR-WP-0009-T03
status: done
priority: high
state_hub_task_id: "f7a29cca-d76c-4205-a33b-3a2276459fce"
```
Environment is already sufficient, verified 2026-08-21:
```text
CLAUDECODE=1
CLAUDE_CODE_SESSION_ID=<uuid>
CLAUDE_PID=<pid>
CLAUDE_CODE_ENTRYPOINT=cli
```
Only the **model** is missing — `CLAUDE_EFFORT` is exported but the model id is
not. Resolve from `~/.claude/settings.json` `model`, or have the hook accept an
`ASSISTANT_MODEL` override the session sets. Note that `settings.json` records
the configured model, which is not always the model that served a given turn.
Once live, retire the `Co-Authored-By` prompt instruction in `~/.claude/CLAUDE.md`
per T01's ruling — a hook cannot be forgotten mid-session the way a prompt
instruction can, which is the main reason to prefer it.
Result (2026-08-21): the hook detects `CLAUDECODE`, session id, and PID, accepts
the neutral overrides, and uses `~/.claude/settings.json`'s configured model as
a fallback. No `Co-Authored-By` instruction remains in the current global
Claude instructions, so no external prompt edit was needed.
## Wire Codex
```task
id: RMGR-WP-0009-T04
status: done
priority: high
state_hub_task_id: "180e50ff-6d43-46f3-84d2-b5337ff73900"
```
Discover what the Codex CLI exports — no `CODEX_*` variables were present in the
Claude session's environment on 2026-08-21, so this needs checking from inside a
Codex session rather than assumed.
Codex is the case that motivates the workplan: it is the assistant currently
invisible in `git log`, and the one that ran 14 hours alongside a Claude session.
If it exports nothing usable, set the variables in its launch wrapper. A stable
per-session value is worth more than the exact PID if only one is available.
Result (2026-08-21): the active Codex CLI exports `CODEX_SESSION_ID` and
`CODEX_THREAD_ID`; the hook uses the former as the stable session and reads the
configured model from `$CODEX_HOME/config.toml` (or `~/.codex/config.toml`) when
no exact neutral override is present. Codex's published stable environment
variable reference does not promise those session variables, so they remain a
best-effort compatibility input rather than the contract.
## Wire Grok
```task
id: RMGR-WP-0009-T05
status: done
priority: medium
state_hub_task_id: "b8451fe8-233f-4862-a238-5ee84901e7fb"
```
Same discovery for Grok. Lowest priority of the three only because it appears
least in current history — confirm that is still true before deciding.
Result (2026-08-21): the installed Grok client exposes `GROK_SESSION_ID` and
`GROK_DEFAULT_MODEL` integration points; the hook consumes both and also accepts
the neutral override contract. No Grok process was active during cutover, so
its next real commit remains the operational proof rather than inferred history.
## Report on assistant activity and collisions
```task
id: RMGR-WP-0009-T06
status: done
priority: medium
state_hub_task_id: "a28d9287-b488-4946-bf46-6cad1a392778"
```
The trailers are the input; this is the payoff. An `rmgr` report over
`git log --format=%(trailers)` answering:
- which assistant and model touched a repo, over a window;
- **overlapping sessions** — two `Assistant-Session` values with interleaved
commits in one repo, which is the collision signal asked for;
- commits with no trailer, split into human (expected) and assistant-not-yet-wired
(a gap to close).
Keep it read-only over git. Do not add a hub table: this is derivable from the
repositories, and `state-hub` is being retired under `STATE-WP-0079`.
Result (2026-08-21): `rmgr assistant-provenance report` derives assistant,
model, process, session, unattributed buckets, and interleaved-session pairs
from Git history only. Regression coverage includes an A→B→A interleave.
## Handle existing history
```task
id: RMGR-WP-0009-T07
status: done
priority: low
state_hub_task_id: "e4ff1ab5-3507-4dff-bb0d-3fed3e0be80f"
```
History is not rewritten — the same reasoning as `ADR-007` § Migration option 2.
Commits before the cutover stay unattributed, and the report must present that
honestly as "unknown", never infer an assistant from timing or message style.
Record the cutover commit so the report can distinguish *"before provenance
existed"* from *"assistant not wired"*. Without that boundary every pre-cutover
commit reads as a gap.
Also decide the `custodian-sync` identity's treatment: it is automation with its
own committer identity already, so it likely needs no trailer — but it should be
named in the standard so it is not later mistaken for an unwired assistant.
Result (2026-08-21): history remains untouched and `custodian-sync` is
declared known automation exempt from assistant trailers. The configuration and
report distinguish before/after cutover at first attributed commit
`329af60753b20896f30f098e8f75074652780032`. A naturally occurring
multi-session window remains an acceptance proof for the plan, not a reason to
misclassify the cutover task as incomplete.
## Acceptance
- [x] Trailer contract published in `docs/repository-standards_v0.1.md`
- [x] Hook live via `core.hooksPath`; never blocks, idempotent, silent for humans
- [x] Claude, Codex, and Grok each emit trailers, or are recorded as unable with a reason
- [x] `git log` alone answers "which assistant, which model, which session"
- [ ] Overlapping-session report runs against at least one real collision window
- [x] Cutover commit recorded; pre-cutover history reported as unknown, not inferred