Skip to content

Sign the commits a run's own workers make - #1495

Merged
AbirAbbas merged 6 commits into
devfrom
fix/worker-commit-attribution
Sep 25, 2026
Merged

AbirAbbas merged 6 commits into
devfrom
fix/worker-commit-attribution

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

What was wrong

On the default task belt (the worker harness, what /task and codeaf do use unless CODEAF_TASK_BELT=node), a commit the task worker made itself carried no attribution lines. The harness signed only work still uncommitted when the run landed, so /task "create hello.txt containing hi and commit it" or codeaf do "…and commit it" put an unsigned commit on the person's branch, while the manual said a task landed on the default belt carries Assisted-by: CodeAF.

Two adjacent faults on the same road were fixed in the same change:

  • the harness signed its own landing commit even in a repository whose CONTRIBUTING forbids AI trailers (only the model was told to obey CONTRIBUTING);
  • a run whose worker committed everything answered nothing to land while comeHome merged its work into the person's checkout.

Design (harness-side)

When a run ends, before its work comes home, the harness walks the commits the run made on the branch its working copy stands on since the run's base and gives each one that lacks them the same lines the landing writes: the bare Assisted-by: CodeAF and the co-author (exec.SignCommitMessageOnce, through gitSignature). It is a message-only rewrite with plumbing: git commit-tree --no-gpg-sign with the original tree, remapped parents, and the original author and committer (names, emails, dates), then one compare-and-swap git update-ref. The tree, the index and the working directory do not move, and no hook runs twice.

Why not a commit-msg hook through GIT_CONFIG_* / core.hooksPath: that environment reaches every git the worker runs (a project's own test repositories would get trailers), and it replaces the repository's own hooks unless every hook is wrapped and chained. Rewriting at the end touches only the run's own unpublished commits, in one place.

  • /task: LandRunTree(dir, base, title, model) signs first, then commits leftovers, then names the paths since the base. RunEngine.Land takes the run's checkBase. The copy record keeps it across a restart; an old record without it signs nothing.
  • codeaf do: still makes no commit of its own. runengine.SignWork(before) signs its workers' commits in place when the run ends. A repository with no commits yet counts all of its first commits. A signing failure prints one codeaf: line on stderr and never changes the exit status.
  • One repository rule: ContributingRefusesTrailers (a conservative text reading: a sentence must name attribution, name AI and prohibit it; a requirement to add or keep the lines, including "do not remove", always wins) plus repositoryRefusesTrailers, which reads the first 256 KiB of regular CONTRIBUTING{,.md,.rst,.txt} files at the repository's top level, in .github/ and in docs/. It controls the rewrite and the landing's own commit (commitTaskWorkAs).
  • Limits (stated in the manual): a range containing any gpgsig/mergetag/encoding header is left byte-for-byte as it is. A commit that another branch, a tag, a remote-tracking ref or FETCH_HEAD reaches is never rewritten (pushed, tagged, other-branch or fetched-and-merged work). A detached HEAD rewrites nothing. codeaf do works in place, so a commit a person makes in that folder during the run is signed too.
  • The prompt line for the bash worker page was tried as an extra and dropped: it pushed the page over its existing 16 KiB cap, and the fix does not need it.

Validation contract, and how each item is proven

Each test uses real temp git repositories through the real landing path; only the model is faked. The new regression tests were each seen failing before the fix.

  1. Every commit a run's worker made that lands on the person's branch carries the same attribution lines as the harness's own landing commit. TestLandSignsWorkerCommitAndNamesCommittedWork (real Supervisor, then run.Land, with and without leftover work). TestBeltRunSignsWorkerCommitsInGround (the /task door through comeHome: two worker commits plus leftovers, all three signed once on the ground's branch). TestLandRunTreeSignsWorkerCommitsAndNamesTheirPaths.
  2. Never doubled. TestSignRunCommitsCompletesOnceAndKeepsWorkerIdentity (a second pass rewrites 0; a message with one line gains only the other). TestSignRunCommitsKeepsAlreadySignedWorkerSHA. TestSignCommitMessageOncePreservesAndCompletesWorkerTrailers. TestSignRunCommitsKeepsRawWorkerMessageAndCommitIdentity.
  3. Commits the run did not make are never rewritten. TestSignRunCommitsKeepsOtherRefsAndMergeParents (base history, a merged outside branch, a tag, a remote-tracking ref). TestSignRunCommitsKeepsFetchedMergeParent (FETCH_HEAD). TestRunCommitIdentityKeepsAngleBracketInName.
  4. A repository whose CONTRIBUTING forbids trailers gets none. TestContributingRefusesTrailersOnlyForAIBans (a table checked in both directions). TestRepositoryRefusesTrailersReadsOnlyRegularContributingFiles. The CONTRIBUTING arms of TestLandRunTreeSignsWorkerCommitsAndNamesTheirPaths (the worker's commit and the landing's own commit stay unsigned) and of TestDoOnTheRunEngineSignsWorkerCommitsUnlessContributingForbids.
  5. The repository's own hooks still run. In TestLandSignsWorkerCommitAndNamesCommittedWork and TestLandRunTreeSignsWorkerCommitsAndNamesTheirPaths, a real pre-commit hook writes a marker when the worker commits. The landing does not run it again, and core.hooksPath / local config are unchanged.
  6. codeaf do behaves the same as /task. TestDoOnTheRunEngineSignsWorkerCommitsUnlessContributingForbids (a scripted bash worker runs git add hello.txt && git commit through runErrand; the commit is signed once with bare lines, the person's earlier commit keeps its sha, the envelope names hello.txt, exit 0). TestDoOnTheRunEngineSignsFirstCommitOnUnbornBranch. TestRunTreeSnapshotSignsFirstCommitsOnUnbornBranch.
  7. A worker-committed run reports landed work. landed on run-work: 1 file in TestLandSignsWorkerCommitAndNamesCommittedWork. TestLandNamesCommittedThenRevertedWork / TestLandRunTreeNamesCommittedThenRevertedWork cover a commit followed by its revert. A read-only run still answers nothing to land (TestLandRunTreeWithBaseRefusesReadOnlyRun, TestLandRefusesARunThatWroteNothing).
  8. Limits hold. TestSignRunCommitsLeavesProtectedAndUnbasedHistoryUntouched covers a crafted gpgsig range (left byte-for-byte), a detached HEAD and an empty base.

Hand check: the real binary built from this branch

Setup: make build; a throwaway home with every model seat pinned to deepseek/deepseek-v4-flash, CODEAF_MODEL_POOL=read, telemetry off. Total spend was about $0.01.

/task create hello.txt containing hi and commit it in the TUI, in a scratch repo on branch work that has a pre-commit hook writing a marker. The task row read done · 56s · 1 file · merged and the run's note read landed on work: 1 file.

$ git log -1 --format=%B work
add hello.txt

Assisted-by: CodeAF
Co-Authored-By: CodeAF <267109073+agentfield-bot@users.noreply.github.com>

$ git reflog -4 work
0308740 work@{0}: merge task/create-hello-txt-containing-hi-a-674cce: Fast-forward
289fcc5 work@{1}: commit (initial): start task-repo2
marker: pre-commit ran 1790347384        core.hooksPath: (unset)

The same /task run in a repo checked out on main kept its branch, which is the existing protected-branch rule (your checkout is on main, which tasks do not merge into automatically). On that kept branch the worker commit aca75ec (author agent1) became 707db24 (codeaf: sign the run's own commits), with the lines once and the author unchanged.

codeaf do --dir <repo> --yes-spend --json … "create hello.txt containing hi and commit it" in a repo with a pre-commit hook. Result: ok: true, stop: done, files: [".../hello.txt"], spend $0.002.

$ git log --format='--- %h %an | %cn%n%B'
--- 698f853 Person | Person
add hello.txt with hi

Assisted-by: CodeAF
Co-Authored-By: CodeAF <267109073+agentfield-bot@users.noreply.github.com>
--- f6a50f1 Person | Person
start do-repo

$ git reflog -3
698f853 HEAD@{0}: codeaf: sign the run's own commits
0d26ca0 HEAD@{1}: commit: add hello.txt with hi
f6a50f1 HEAD@{2}: commit (initial): start do-repo
marker: pre-commit ran 1790346707

codeaf do … in a repo whose CONTRIBUTING.md says "Do not add AI co-author trailers to commits.": ok: true, and nothing was rewritten.

--- 79e0d63 Person
add hello.txt

--- a5799dc Person
start ban-repo

codeaf do … in a freshly git inited repo (no commits): the worker's first commit Add hello.txt carries the two lines once.

Manual and change entry

  • permissions.md now says the worker harness signs its workers' own commits once, with the bare line, when the run ends, and that codeaf do does the same in place. It lists when a commit is left alone, and says codeaf reads CONTRIBUTING itself and names the files it reads. It no longer promises a "tells you it did" sentence the harness does not say. The signing section was split so each section answers its own heading.
  • worker-harness.md no longer says do commits nothing or that a worker-committed run lands nothing, and fixes a stale "How to turn it on" pointer. It also adds a heading for when a task's work comes home.
  • Two probes in a person's own words reach permissions.md.
  • docs/changes/unreleased/1495-worker-commits-signed.md states what was true and what is true now.

Gates on the head after merging origin/dev: make pr-ready's light gate (build, vet, fmt, packed manual, change entries, manual gates, laws) passes. The fresh touched-package run was taken on a shared box at load average 12–14. A handful of internal/run and internal/session tests failed only under that load, and a different handful on each run. They are timing and concurrency tests the machine governor holds back on a busy host (e.g. peak concurrency = 5 … want all 6). They are not in code this change touches, and each passes when run on its own. CI's touched packages is the clean-runner answer.

🤖 Generated with Claude Code

AbirAbbas and others added 6 commits September 24, 2026 23:47
…ber)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SignCommitMessageOnce leaves a message that already carries both codeaf
lines byte for byte, gives a message with neither the same two-line block
SignCommitMessage writes, and gives a message with one of them only the
line it lacks. ContributingRefusesTrailers is the one conservative reading
of a repository's ban on AI trailers: a sentence is a ban only when it
names attribution, names AI and prohibits, and a requirement to add or
keep the lines (including "do not remove") always wins.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…work they committed

On the worker harness a commit the worker made itself carried no
attribution: the landing signed only the work still uncommitted when the
run ended. Now, before its work goes home, the landing walks the commits
the run made on its own branch since its base and gives each one that
lacks them the same bare Assisted-by and co-author lines, as a
message-only rewrite (commit-tree with the original tree, author and
committer, then one compare-and-swap update-ref), so the tree, the index
and the repository's own hooks are untouched. Commits another branch,
tag, remote-tracking ref or FETCH_HEAD reaches, and a range holding a
gpgsig/mergetag/encoding header, are left exactly as they are.

A repository whose CONTRIBUTING forbids AI trailers gets none, from the
rewrite or from the landing's own commit. The copy record keeps the run's
base across a restart, and a run whose worker committed everything (or
committed and reverted) now names its branch and files instead of saying
nothing to land.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
codeaf do still makes no commit of its own in the folder it was handed.
When its run ends it signs the commits its workers made there since the
run began (every commit on the branch when the folder had none), with
the landing's bare line, before it names the files in the envelope. A
signing failure is one codeaf: line on stderr and never changes the exit
status; the work is on disk either way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
permissions.md says the worker harness signs its workers' commits too,
once, with the bare line, and when it leaves one alone (a GPG/SSH-signed
range, a pushed, tagged, other-branch or fetched commit, a detached
HEAD); that codeaf now reads CONTRIBUTING itself, and which files; and no
longer promises a sentence the harness does not say. The do and /task
pages stop saying a run commits nothing or lands nothing when its worker
committed. Two probes in a person's own words reach the page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas marked this pull request as ready for review September 25, 2026 15:12
@AbirAbbas
AbirAbbas merged commit 7f136b3 into dev Sep 25, 2026
4 checks passed
@AbirAbbas
AbirAbbas deleted the fix/worker-commit-attribution branch September 25, 2026 15:22
ZeroPoint95 added a commit that referenced this pull request Sep 25, 2026
Resolves the two conflicts #1495 made: Land takes the run's base and the
copy record carries CheckBase beside Continues. The clock test's slow
landing takes the base too, and places.md says where the here ~/codeaf
chip sat, so the manual's search still reaches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas added a commit that referenced this pull request Sep 25, 2026
…and #1495

Four tui3 conflicts, each #1429's wall and team code beside the plan page
this branch removes. Both are kept: the wall's paint term stands before the
task room's, the wall, team menu and zoom keys are read where the plan page's
and work tab's keys were, the row keeps both its plan and open fields, and the
frame draws the wall where it drew the plan page and the work tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AbirAbbas added a commit that referenced this pull request Sep 25, 2026
Merged through a commit that stands for #1429's squash with the branch this
one was built on (6416a6c) as its second parent, so only the real
conflicts show.

tabtitle: #1496 put the hovered tab's full title on the head's blank row under
the rule; on this branch a chat's head is the nav, the strip, the rule and the
blank, so that row is chatHeadRows-1 and the strip's row is tabStripRow.

notice: #1487's quiet tip reads the keys row of a put-away column, which this
branch spells through sideBackHint (`alt+l tasks`, `alt+l traffic` for a
manager) rather than the retired railBackHint.

manual: #1495's workers heading stands before this branch's rewritten roster
paragraph; team_stop keeps #1429's background-member sentence beside this
branch's new rows; the dock and wall probes are both kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant