repo-manager/workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md
codex c3f4ece7ea fix(workplans): adopt ADR-007 derived identifiers for unregistered records
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.

Records central already holds were deliberately left untouched.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:21:52 +02:00

12 KiB

id type title domain repo status owner topic_slug created updated parent_project related state_hub_workstream_id
RMGR-WP-0009 workplan Coding-assistant commit provenance trailers infotech repo-manager finished codex infotech 2026-08-21 2026-08-21 prj-state-hub-retirement
RMGR-WP-0004
RMGR-WP-0005
ADR-007
ADR-010
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:

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

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:

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

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

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:

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

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

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

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

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

  • Trailer contract published in docs/repository-standards_v0.1.md
  • Hook live via core.hooksPath; never blocks, idempotent, silent for humans
  • Claude, Codex, and Grok each emit trailers, or are recorded as unable with a reason
  • git log alone answers "which assistant, which model, which session"
  • Natural overlapping-session proof handed off as residual RMGR-IN-0001
  • 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.