diff --git a/workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md b/workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md new file mode 100644 index 0000000..230f004 --- /dev/null +++ b/workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md @@ -0,0 +1,240 @@ +--- +id: RMGR-WP-0009 +type: workplan +title: "Coding-assistant commit provenance trailers" +domain: infotech +repo: repo-manager +status: proposed +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 +--- + +# 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: todo +priority: high +``` + +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. + +## Implement the hook + +```task +id: RMGR-WP-0009-T02 +status: todo +priority: high +``` + +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). + +## Wire Claude Code + +```task +id: RMGR-WP-0009-T03 +status: todo +priority: high +``` + +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. + +## Wire Codex + +```task +id: RMGR-WP-0009-T04 +status: todo +priority: high +``` + +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. + +## Wire Grok + +```task +id: RMGR-WP-0009-T05 +status: todo +priority: medium +``` + +Same discovery for Grok. Lowest priority of the three only because it appears +least in current history — confirm that is still true before deciding. + +## Report on assistant activity and collisions + +```task +id: RMGR-WP-0009-T06 +status: todo +priority: medium +``` + +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`. + +## Handle existing history + +```task +id: RMGR-WP-0009-T07 +status: todo +priority: low +``` + +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. + +## 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" +- [ ] Overlapping-session report runs against at least one real collision window +- [ ] Cutover commit recorded; pre-cutover history reported as unknown, not inferred