LearnNewsExamplesServices
Frontmatter
id16143
titleAuthorship pre-push guard covers linked worktrees only, not clones
stateClosed
labels
bugdeveloper-experienceai
assigneesneo-gpt-emmy
createdAtJul 29, 2026, 11:00 PM
updatedAtJul 30, 2026, 12:11 AM
githubUrlhttps://github.com/neomjs/neo/issues/16143
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 30, 2026, 12:11 AM

Authorship pre-push guard covers linked worktrees only, not clones

Closed Backlog/active-chunk-10 bugdeveloper-experienceai
neo-opus-vega
neo-opus-vega commented on Jul 29, 2026, 11:00 PM

Context

Found 2026-07-29 while reviewing PR #16142 (fix(ai): bind worktree git identity (#15337)), then confirmed against live board state after that PR merged.

The operator observed that 2 of the last 214 commits were attributed to him and asked what they were. Investigation showed dev is clean — all 214 commits carry correct agent authorship, and GitHub's squash-merge preserves the PR author, so branch-level misattribution does not reach dev. Verified: 973863610c (the merged #16137) is authored Emmy <neo-gpt-emmy@neomjs.com> even though its branch head bacef9d1ff was authored tobiu <tobiasuhlig78@gmail.com>.

The misattributed commits live on branch heads, and one is live: a989e491ba, authored tobiu <tobiasuhlig78@gmail.com> at 2026-07-29 19:42:42 +0200, is the current head of the open PR #16138 (confirmed via GET /repos/neomjs/neo/pulls/16138/commits, not only local refs). That commit was created after the #15338 pre-push guard shipped and reached a PR head anyway.

The Problem

#15337's fix covers two checkout topologies. Its mechanical backstop covers one.

buildScripts/util/check-commit-authorship.mjs (184 lines, wired into .husky/pre-push) scopes itself explicitly in its own doc comment at :23:

"In a LINKED worktree (git-dir !== git-common-dir — i.e. .git/worktrees/<name>, which is how every agent worktree is created), no pushed commit may carry the identity from the operator's GLOBAL git config."

and implements exactly that at :96:

return path.resolve(gitDir) !== path.resolve(commonDir)

An explicit independent clone has gitDir === commonDir, so that predicate returns false and the guard's rule does not apply to it.

Meanwhile #15337's own Contract Ledger carries a separate row for that topology — "Explicit independent clone | --canonical-root topology | write both values with clone-local config" — and PR #16142 implemented it: the bootstrap binder writes clone-local config for an independent clone and worktree-scoped config for a linked worktree.

So the asymmetry is:

linked worktree explicit independent clone
bootstrap identity binding (#16142)
pre-push backstop (#15338) not covered

A seat on an independent clone now gets the prevention and no backstop. #15337 rejected exactly this shape of reliance: "Every gate in this repo that relies on remembering has failed at least once tonight; the <noreply@*> rule is mechanical for exactly this reason."

The second half is the same fix. PR #16142 is forward-only by design — it binds identity at bootstrap, so a seat provisioned before it merged keeps the operator's global identity until re-bootstrapped. Its Post-Merge Validation correctly says "on the next fresh resident linked-worktree bootstrap." Nothing covers already-provisioned seats. Widening the guard's topology coverage closes that too: a stale seat stops being silently misattributed and starts being loudly refused at push, which is the only signal that reaches the agent in time.

Observation vs inference, kept separate. Verified: the guard's scope is narrower than the fix's scope, and an operator-authored commit reached an open PR head today after the guard existed. Not verified: why that specific commit was not refused. Three live candidates, none confirmed — the pushing seat is an independent clone (the scope gap above); git push --no-verify was used (a documented bypass at :32); or the seat predates the hook being installed. Determining which is part of this ticket's work, not a premise of it.

The Architectural Reality

  • buildScripts/util/check-commit-authorship.mjsisLinkedWorktree() (:79:96) is the topology predicate; the refusal path and its offender report live at :159. The comparison at :28 is deliberately against the global config rather than a roster: "the leak IS the global identity resolving through an unset local one, so that value is exactly the thing to detect." That reasoning is topology-independent — it holds for a clone just as it does for a worktree.
  • .husky/pre-push — the invocation site.
  • ai/scripts/migrations/bootstrapWorktree.mjs — the #16142 binder; :244 already names this guard as "the mechanical backstop," which is the contract this ticket makes true for both topologies.
  • The independent-clone topology is a first-class deployment shape, not hypothetical: --canonical-root / NEO_AI_CANONICAL_ROOT exist precisely to support per-peer clones that mirror the canonical checkout.

The Fix

  1. Widen the guard's applicability from "linked worktree" to "any checkout that is not the operator's main checkout." The existing predicate answers a topology question when the rule needs an ownership question. isLinkedWorktree() becomes one branch of that decision rather than the whole of it.
  2. Establish how an independent clone identifies itself as agent-owned rather than operator-owned. NEO_AGENT_IDENTITY is the candidate already used by the #16142 binder for exactly this discrimination, which keeps one source of authority across prevention and backstop. Resolve this explicitly rather than by inference — it is the one genuine design decision here.
  3. Preserve the documented --no-verify bypass and the global-config comparison unchanged. Neither is the defect.
  4. If a topology genuinely cannot be covered, say so in the guard's doc comment with the reason, so the next reader does not have to re-derive the gap from a live misattributed commit.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Evidence
isLinkedWorktree() (check-commit-authorship.mjs:79) git topology (--absolute-git-dir vs --git-common-dir) remains correct for what it measures; stops being the sole gate on rule applicability unchanged for linked worktrees existing linked-worktree witnesses stay green
guard applicability decision agent-ownership, not topology refuse operator-global-authored commits from any agent-owned checkout topology undecidable → fail closed and say why new witness: independent clone with operator identity → push refused
agent-ownership discrimination NEO_AGENT_IDENTITY (same authority #16142's binder uses) one source of authority shared by prevention and backstop absent identity → treat as operator checkout, do not refuse witness: main checkout unaffected
existing-seat remediation the widened guard itself stale pre-#16142 seat fails loud at push instead of misattributing silently none needed — refusal is the remediation path witness: seat with global identity and no local binding → refused
--no-verify bypass (:32) documented operator escape unchanged n/a existing behavior preserved

Decision Record impact

none. This corrects a scope gap in an existing guard; it introduces no new authority and no ADR governs the hook layer. Aligned-with the attribution-honesty value established by #15337 and by §critical_gates rule 4 (the <noreply@*> co-author ban), which exists for the same reason in the adjacent direction.

Acceptance Criteria

  • An independent clone (gitDir === commonDir) carrying the operator's global identity has its push refused, with the same offender report linked worktrees already produce
  • The operator's own main checkout is still never refused — a real witness, not an assumed one, since this is the regression the widening could introduce
  • A linked worktree's existing behavior is unchanged (existing witnesses stay green)
  • Agent-ownership discrimination reads from a single named authority shared with bootstrapWorktree.mjs, not a second independent resolver
  • The --no-verify bypass and the global-config comparison are preserved unchanged
  • A seat provisioned before PR #16142 — identity unbound, resolving to the operator's global config — is refused at push rather than silently misattributing
  • Any topology deliberately left uncovered is named in the guard's doc comment with its reason
  • The three unconfirmed candidates for a989e491ba (independent clone / --no-verify / pre-hook seat) are narrowed to the actual mechanism, and the finding recorded — the fix must not rest on the wrong one

Out of Scope

  • Rewriting history. dev is clean (214/214 correctly attributed); branch-level commits are repaired by squash-merge on the way in.
  • Repairing a989e491ba on PR #16138 specifically — that is Emmy's PR and its author's call; squash-merge will land it as hers regardless.
  • The bootstrap binder itself (#15337 → PR #16142, merged). This ticket touches only the backstop.
  • The <noreply@*> co-author gate — already mechanical.
  • Re-bootstrapping peer seats as an operational task. If the widened guard proves that necessary, it is operator/peer authority and belongs in its own artifact.

Avoided Traps

  • Do not treat this as "#16142 was incomplete." It delivered its ledger row for both topologies. The gap is in the older backstop, which predates the independent-clone row existing.
  • Do not assert why a989e491ba escaped. Three candidates fit the symptom; picking one without evidence would be a confabulated cause, and a fix aimed at the wrong one leaves the hole open. The doc comment at :32 documents a legitimate bypass, so "the guard is broken" is not the only reading.
  • Do not add a second identity resolver. #16142's binder already resolves agent identity; a parallel resolver in the guard could disagree with it, which is the failure ADR 0019 §10.1 retired the "twin" shape over.
  • Do not conflate dev cleanliness with correctness. 214/214 clean on dev measures what squash-merge repairs, not what the guard prevents. The counter aggregates over both outcomes.
  • Do not fix this by documentation. #15337 rejected a docs-only fix for the parent defect on the grounds that nothing warned across 38 commits and dozens of log reads. The same reasoning applies to its backstop.

Related

  • Parent: #15337 (operator-identity inheritance) → PR #16142 (bootstrap binder, merged 2026-07-29)
  • The backstop this ticket widens: #15338
  • The review that surfaced it: PR #16142 review — the coverage boundary I stated there ("AC5 taken from the 19/19 targeted run rather than reading the guard myself") is exactly where this gap sat
  • §critical_gates rule 4 — the <noreply@*> co-author ban, the adjacent already-mechanical attribution rule
  • ADR 0019 §10.1 — the retired "twin" shape, cited above as the reason not to add a second identity resolver

Live latest-open sweep: checked the latest 20 open issues plus an all-state search for authorship worktree identity clone guard immediately before filing on 2026-07-29 — only #15337 (CLOSED, the parent) matched; no equivalent open ticket. A2A in-flight sweep: 25 most recent messages, all read-states — recent [lane-claim]s are #16113 and #16128 (both @neo-gpt-emmy, unrelated scope); no competing claim on commit attribution.

Origin Session ID: 0a7f5f1d-cf12-4698-984c-17b64eea5178

Retrieval Hint: check-commit-authorship linked worktree independent clone scope gap operator identity pre-push backstop isLinkedWorktree gitDir commonDir

tobiu referenced in commit be17257 - "fix(build): cover agent-owned independent clones (#16143) (#16146) on Jul 30, 2026, 12:11 AM
tobiu closed this issue on Jul 30, 2026, 12:11 AM