Context
Surfaced 2026-08-01 by @tobiu, who noticed agent commits mapping to an unexpected GitHub user. The reported symptom did not reproduce — every commit on dev in the last 26h resolves correctly through the GitHub API, author and committer — but the investigation found a real defect one field over, in Co-Authored-By trailers.
Everything in Context and The Problem is observed. The Fix is prescription and is marked as such.
Measured on origin/dev with trailer-anchored --fixed-strings matches (an unanchored ada@neomjs.com grep double-counts, because it is a substring of neo-opus-ada@neomjs.com — the first count I took was wrong for exactly that reason):
| trailer address |
commits |
status |
<neo-opus-4-7@neomjs.com> |
76 |
correct — the roster address |
<neo-opus-ada@neomjs.com> |
18 |
derived from the GitHub handle |
<ada@neomjs.com> |
1 |
derived from the display name |
So 19 commits credit a co-author at an address that is not the canonical one. The 18 span 2026-06-13 → 2026-07-03; the single display-name one is 2026-07-25.
The Problem
The canonical roster documents 3 of the 9 agent addresses actually in use, so a peer who needs to co-author with anyone else has nothing to read and derives one.
.agents/skills/pull-request/references/pull-request-workflow.md line 104 is the source of authority. It names exactly three:
neo-opus-4-7@neomjs.com, neo-gemini-3-1-pro@neomjs.com, neo-gpt@neomjs.com
Live usage on dev over the last 60 days is nine:
neo-claude-opus neo-fable-clio neo-fable neo-gpt-emmy neo-gpt
neo-kimi-iris neo-kimi-phoebe neo-opus-4-7 neo-opus-vega
Six of nine are undocumented. The roster also still lists neo-gemini-3-1-pro, which has no commits in the window.
Two things make this a substrate defect rather than carelessness:
- The rule is unenforceable as written. "Read the roster, never derive" cannot be followed when the roster does not contain the address. Both derivations here are reasonable guesses — one from the display name, one from the GitHub login — produced by someone doing the right thing with an incomplete source.
- The roster is buried. Line 104 is not a roster section; it is a sub-clause of a
**FORBIDDEN:** bullet about <noreply@*> footers. Someone scanning for "what address do I credit Emmy at" does not find it there.
The consequence is silent. A trailer at an unowned address renders as an unlinked name, so agent participation — which the same line 104 calls a long-term distributed-memory and RLAIF substrate — is simply lost for those commits, with nothing failing.
Amendment 2026-08-01 (author, in place)
The original Fix proposed a new roster data file. That was wrong and is withdrawn. A prior-art sweep at implementation time — which I should have run at authoring time, not after filing — found the registry already exists:
ai/graph/identityRoots.mjs describes itself as "the definitive addressable identity surface" and, in deriveFleetRoster.mjs's words, "the one durable registry". It carries githubLogin for all 11 seats (10 agents + @tobiu) and zero email fields.
buildScripts/util/deriveFleetRoster.mjs already imports IDENTITIES from it — precedent for a build script consuming the registry, in the same directory as the intended consumer, with a --check CI guard against hand-painting.
Creating a second roster would have put a competing identity list next to a module that explicitly claims to be the only one. Substrate stage of the challenge chain, caught one gate late.
Two corrections to the numbers below, both from that sweep:
- The sunset condition said three local parts differ from their login "plus the Fable seats". Wrong —
neo-fable and neo-fable-clio both match. It is two: mine (neo-opus-4-7 → @neo-opus-ada) and @neo-opus-grace's (neo-claude-opus → @neo-opus-grace). Seven of nine already satisfy localPart === githubLogin, so the derivation rule that retires this work is two registrations away, not four.
- The revised Fix does not add
email to identityRoots.mjs. That module seeds Memory Core graph nodes, so a field added there becomes an ingested, queryable node property — real blast radius for what is a git-tooling concern, and the module deliberately retired era-owned facts to stay lean. deriveFleetRoster.mjs already faced this exact question for engine tags and solved it with "a small explicit map whose every entry names its source anchor", next to the consumer. This follows that precedent.
Revised Fix: a githubLogin → email map beside check-commit-authorship.mjs, keyed by and validated against IDENTITIES so a login typo or a newly-seeded identity missing an address fails loudly rather than silently omitting a seat; pull-request-workflow.md cites it; the pre-push check validates trailers against it. Everything below about warn-never-block, fail-open, and the sunset trigger stands unchanged.
The Architectural Reality
.agents/skills/pull-request/references/pull-request-workflow.md:104 — the roster, as prose, inside a FORBIDDEN bullet. Source of authority today.
buildScripts/util/check-commit-authorship.mjs — pre-push, wired via .husky/pre-push. It guards the author identity against operator-leak and explicitly declines a roster: "Comparing against the global config rather than a hard-coded roster is deliberate … A roster would need maintaining, and would miss any operator this repo is cloned by."
That reasoning is correct for its own case and does not extend to trailers: a trailer names a peer, not the pusher, so the pusher's git config cannot validate it. This is the gap, not a contradiction of the existing design.
.husky/pre-push — already runs check-branch-discipline.mjs and check-commit-authorship.mjs; the channel exists and reaches the author.
- GitHub resolves commit and trailer identity by email lookup, which is why this is invisible locally and only observable through the API.
The Fix
(Prescription — inference, not observation.)
Make the roster data, not prose, and have the pre-push check read the same file the doc does.
- A machine-readable roster listing every active agent identity as
{githubLogin, email, displayName}.
pull-request-workflow.md cites that file as the source of authority instead of inlining three addresses, so the doc cannot drift from what the lint enforces.
- Extend
check-commit-authorship.mjs to validate Co-Authored-By trailer addresses against the roster, in the same non-blocking-vs-blocking posture the file already uses for its author check.
Two properties any implementation should keep:
- Do not break the clone case. The existing author check works for any operator cloning this repo precisely because it uses their own config. Trailer validation must not make an unknown address fatal for an outside contributor — an unrecognized trailer is a warning, not a wall.
- Fail toward pushing on an unreadable roster. A missing or malformed roster file must not block a push; it degrades to today's behaviour.
Sunset condition (substrate accretion defense). Three of the nine local parts do not equal their GitHub login (neo-opus-4-7→neo-opus-ada, neo-claude-opus→neo-opus-grace, plus the Fable seats). Once every address satisfies localPart === githubLogin, the roster file can retire and the check becomes a derivation rule — validate that the local part is a known login, no list to maintain. Retirement trigger: the first day all active seats satisfy that invariant. This ticket should not be read as endorsing a permanent hand-maintained list.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| agent identity roster (new data file) |
this ticket; live %ae usage on dev |
one row per active seat: login, email, display name |
unreadable ⇒ trailer check no-ops |
cited from pull-request-workflow.md |
file parses; row count matches live-usage sweep |
pull-request-workflow.md:104 |
.agents/skills/pull-request/references/ |
cites the roster file instead of inlining 3 addresses |
n/a — docs |
itself |
no literal address list remains inline |
check-commit-authorship.mjs trailer check |
.husky/pre-push, existing author-check precedent |
warn on a Co-Authored-By address absent from the roster |
unknown address ⇒ warn, never block; missing roster ⇒ silent |
in-script message |
a commit trailing <ada@neomjs.com> warns; one trailing <neo-opus-4-7@neomjs.com> does not |
Decision Record impact
none. This documents existing identities and adds an advisory to existing push tooling; it changes no authorship policy and no ADR authority.
Acceptance Criteria
Out of Scope
- Rewriting history. The 19 mis-credited commits are not fixed by this ticket. 18 of them carry
neo-opus-ada@neomjs.com, which repairs itself if that address is registered to the neo-opus-ada account — an operator action already in motion, tracked separately, and needing no git surgery.
- Changing anyone's commit identity. Whether a seat migrates to a login-matching address is that seat's decision, not this ticket's.
- The existing author-identity check's behavior. Its operator-leak guard and its deliberate no-roster stance for the author field stay exactly as they are.
- Anything about the
<noreply@*> prohibition. Already enforced; untouched here.
Avoided Traps
- "Just derive the address from the handle." Fails today: three of nine local parts do not equal their login. Derivation is the bug, not the fix — until the invariant holds, which is why it is the sunset condition rather than the plan.
- Doc-only update. Rewriting line 104 with nine addresses fixes today and decays the next time a seat joins. The roster went stale precisely because nothing mechanical read it.
- Making unknown trailers fatal. Would break outside contributors and anyone co-authoring with a human, for a failure whose cost is a lost attribution rather than a broken build.
- Folding this into the author check's existing rationale. That file argues against a roster on purpose and it is right for the author field; treating the two cases as one would either weaken its guard or wrongly import its conclusion.
Related
#16256 / PR #16276 — sibling pre-push advisory, same file and same non-blocking posture
#13652 — mechanical-enforcement epic; same philosophy of replacing discipline with a hook
#15337, #16143 — the implementing tickets behind check-commit-authorship.mjs
Origin Session ID: 56105163-6e66-44b6-8c6f-9e81bc1be08c
Retrieval Hint: query_raw_memories("co-author trailer derived address roster stale agent emails"), or buildScripts/util/check-commit-authorship.mjs.
Context
Surfaced 2026-08-01 by @tobiu, who noticed agent commits mapping to an unexpected GitHub user. The reported symptom did not reproduce — every commit on
devin the last 26h resolves correctly through the GitHub API, author and committer — but the investigation found a real defect one field over, inCo-Authored-Bytrailers.Everything in Context and The Problem is observed. The Fix is prescription and is marked as such.
Measured on
origin/devwith trailer-anchored--fixed-stringsmatches (an unanchoredada@neomjs.comgrep double-counts, because it is a substring ofneo-opus-ada@neomjs.com— the first count I took was wrong for exactly that reason):<neo-opus-4-7@neomjs.com><neo-opus-ada@neomjs.com><ada@neomjs.com>So 19 commits credit a co-author at an address that is not the canonical one. The 18 span 2026-06-13 → 2026-07-03; the single display-name one is 2026-07-25.
The Problem
The canonical roster documents 3 of the 9 agent addresses actually in use, so a peer who needs to co-author with anyone else has nothing to read and derives one.
.agents/skills/pull-request/references/pull-request-workflow.mdline 104 is the source of authority. It names exactly three:Live usage on
devover the last 60 days is nine:Six of nine are undocumented. The roster also still lists
neo-gemini-3-1-pro, which has no commits in the window.Two things make this a substrate defect rather than carelessness:
**FORBIDDEN:**bullet about<noreply@*>footers. Someone scanning for "what address do I credit Emmy at" does not find it there.The consequence is silent. A trailer at an unowned address renders as an unlinked name, so agent participation — which the same line 104 calls a long-term distributed-memory and RLAIF substrate — is simply lost for those commits, with nothing failing.
Amendment 2026-08-01 (author, in place)
The original Fix proposed a new roster data file. That was wrong and is withdrawn. A prior-art sweep at implementation time — which I should have run at authoring time, not after filing — found the registry already exists:
ai/graph/identityRoots.mjsdescribes itself as "the definitive addressable identity surface" and, inderiveFleetRoster.mjs's words, "the one durable registry". It carriesgithubLoginfor all 11 seats (10 agents +@tobiu) and zero email fields.buildScripts/util/deriveFleetRoster.mjsalready importsIDENTITIESfrom it — precedent for a build script consuming the registry, in the same directory as the intended consumer, with a--checkCI guard against hand-painting.Creating a second roster would have put a competing identity list next to a module that explicitly claims to be the only one. Substrate stage of the challenge chain, caught one gate late.
Two corrections to the numbers below, both from that sweep:
neo-fableandneo-fable-clioboth match. It is two: mine (neo-opus-4-7→@neo-opus-ada) and @neo-opus-grace's (neo-claude-opus→@neo-opus-grace). Seven of nine already satisfylocalPart === githubLogin, so the derivation rule that retires this work is two registrations away, not four.emailtoidentityRoots.mjs. That module seeds Memory Core graph nodes, so a field added there becomes an ingested, queryable node property — real blast radius for what is a git-tooling concern, and the module deliberately retired era-owned facts to stay lean.deriveFleetRoster.mjsalready faced this exact question for engine tags and solved it with "a small explicit map whose every entry names its source anchor", next to the consumer. This follows that precedent.Revised Fix: a
githubLogin → emailmap besidecheck-commit-authorship.mjs, keyed by and validated againstIDENTITIESso a login typo or a newly-seeded identity missing an address fails loudly rather than silently omitting a seat;pull-request-workflow.mdcites it; the pre-push check validates trailers against it. Everything below about warn-never-block, fail-open, and the sunset trigger stands unchanged.The Architectural Reality
.agents/skills/pull-request/references/pull-request-workflow.md:104— the roster, as prose, inside a FORBIDDEN bullet. Source of authority today.buildScripts/util/check-commit-authorship.mjs— pre-push, wired via.husky/pre-push. It guards the author identity against operator-leak and explicitly declines a roster: "Comparing against the global config rather than a hard-coded roster is deliberate … A roster would need maintaining, and would miss any operator this repo is cloned by." That reasoning is correct for its own case and does not extend to trailers: a trailer names a peer, not the pusher, so the pusher's git config cannot validate it. This is the gap, not a contradiction of the existing design..husky/pre-push— already runscheck-branch-discipline.mjsandcheck-commit-authorship.mjs; the channel exists and reaches the author.The Fix
(Prescription — inference, not observation.)
Make the roster data, not prose, and have the pre-push check read the same file the doc does.
{githubLogin, email, displayName}.pull-request-workflow.mdcites that file as the source of authority instead of inlining three addresses, so the doc cannot drift from what the lint enforces.check-commit-authorship.mjsto validateCo-Authored-Bytrailer addresses against the roster, in the same non-blocking-vs-blocking posture the file already uses for its author check.Two properties any implementation should keep:
Sunset condition (substrate accretion defense). Three of the nine local parts do not equal their GitHub login (
neo-opus-4-7→neo-opus-ada,neo-claude-opus→neo-opus-grace, plus the Fable seats). Once every address satisfieslocalPart === githubLogin, the roster file can retire and the check becomes a derivation rule — validate that the local part is a known login, no list to maintain. Retirement trigger: the first day all active seats satisfy that invariant. This ticket should not be read as endorsing a permanent hand-maintained list.Contract Ledger Matrix
%aeusage ondevpull-request-workflow.mdpull-request-workflow.md:104.agents/skills/pull-request/references/check-commit-authorship.mjstrailer check.husky/pre-push, existing author-check precedentCo-Authored-Byaddress absent from the roster<ada@neomjs.com>warns; one trailing<neo-opus-4-7@neomjs.com>does notDecision Record impact
none. This documents existing identities and adds an advisory to existing push tooling; it changes no authorship policy and no ADR authority.Acceptance Criteria
devhistory, each with its GitHub login and display name.pull-request-workflow.mdno longer inlines a partial address list; it cites the roster file as source of authority.neo-gemini-3-1-proentry).Co-Authored-Byaddress is absent from the roster emits a warning naming the address and pointing at the roster file.localPart === githubLoginfor all seats ⇒ retire the file) is recorded in the roster file itself, not only in this ticket.Out of Scope
neo-opus-ada@neomjs.com, which repairs itself if that address is registered to theneo-opus-adaaccount — an operator action already in motion, tracked separately, and needing no git surgery.<noreply@*>prohibition. Already enforced; untouched here.Avoided Traps
Related
#16256/ PR #16276 — sibling pre-push advisory, same file and same non-blocking posture#13652— mechanical-enforcement epic; same philosophy of replacing discipline with a hook#15337,#16143— the implementing tickets behindcheck-commit-authorship.mjsOrigin Session ID:
56105163-6e66-44b6-8c6f-9e81bc1be08cRetrieval Hint:
query_raw_memories("co-author trailer derived address roster stale agent emails"), orbuildScripts/util/check-commit-authorship.mjs.