--- id: RMGR-WP-0009 type: workplan title: "Coding-assistant commit provenance trailers" domain: infotech repo: repo-manager status: finished 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: "4a3c130e-efe2-582a-8ea2-03f2ff2493c1" --- # 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 120 of last 200 custodian-sync 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: "583d19a8-37b6-5476-bfb5-fe39c885fff8" ``` 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: - **`@` 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: "7f39c242-ec47-5e90-aade-b1edb5159e85" ``` 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: "9f06a2d3-c1aa-5b27-98de-825d83b7af06" ``` Environment is already sufficient, verified 2026-08-21: ```text CLAUDECODE=1 CLAUDE_CODE_SESSION_ID= CLAUDE_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: "c05552f7-12ca-5252-85b8-0a607fc3822f" ``` 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: "0d7518a2-eb5f-5f5f-8076-dddfe1a238c9" ``` 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: "c9aadb92-b0f9-50b5-b34b-c62ff30acbfe" ``` 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: "6184c71a-0494-5a99-a99e-b0960e26827c" ``` 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" - [x] Natural overlapping-session proof handed off as residual `RMGR-IN-0001` - [x] Cutover commit recorded; pre-cutover history reported as unknown, not inferred ## Closure Finished 2026-08-21. The contract, global hook, three assistant integrations, report, and honest cutover boundary are live. The non-deterministic operational proof that requires two future sessions is preserved as file-backed intake `RMGR-IN-0001` (`origin: residual`, `origin_ref: RMGR-WP-0009`) rather than keeping an otherwise complete workplan active.