This is the record of an abcd-managed repository

abcdBetav0.16.2 · 2026-11-06

Development record

The managed-repo identity gate: the human is the author of record on every commit, before the first one

intentshippedmajorstandaloneitd-131

Press Release

abcd makes sure the right person is the author of record — before a single commit lands, not after 54 of them. A managed repo can silently commit under the wrong git identity: a repo-local Test User <test@example.com> left over from a sandbox, a committer that differs from the author, or — for an autonomous routine — the harness's own default Claude <noreply@anthropic.com>. Each one puts an identity in the contributor graph that the human never chose, and abcd's own attribution gate rejects it downstream, where it surfaces as an unmergeable-looking PR long after the commits are written. With this gate, ahoy checks the effective commit identity — author and committer, whatever git config or environment produced it — against the identity the repo pins, and when they diverge it does not silently rewrite anything: it proposes the pinned (or global) identity and asks before writing repo-local config. For an autonomous routine — which has no human to ask and defaults to a tool identity — the gate detects the wrong identity and the routine's runner establishes the human one before the first commit, so the routine's work is the human's from the start.

"A sandbox Test User override authored 54 commits before anyone noticed — we had to rewrite history and force-push to unpick it," said Alice, who maintains the repo. "Now ahoy catches a wrong identity before the first commit, and it asks me rather than guessing." Bob, who runs abcd's autonomous bug-hunt, hit the mirror image: "The routine committed as the tool, so every PR it opened failed the attribution check and read as unmergeable — but it wasn't a conflict, it was the author. The gate flags that tool identity, and the runner sets mine before the first commit, so the work is attributed to me."

Why This Matters

Graduated from [[iss-62]]. abcd already has most of the check and none of the establish. ahoy's detectGitIdentity calls identity.EffectiveIdentity, which already resolves the author identity from the environment (GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL) first and falls back to git config — so the sandbox/CI env-override case is caught for the author today, and this intent must not re-claim it. What is actually missing is narrower and real:

  1. The committer is never checked. identity.Check evaluates only the author; a committer that differs from the author, or a GIT_COMMITTER_* override, passes the disk-side gate. CI's check-attribution does check the committer, so the true gap is "the local gate checks author, not committer; CI checks both" — a divergence that lets a wrong committer reach CI before it is caught.
  2. The gate reports; it does not establish. Every gap's fix hint tells the human to set user.name/user.email by hand. Nothing offers to do it — safely, with confirmation — at the moment the repo is prepared, so the wrong identity is a diagnostic the human must act on, not a state the gate corrects.
  3. The autonomous-routine case has no local establish path. A cloud routine runs with the harness's default Claude <noreply@anthropic.com> identity and no human to confirm a write; the only guard today is a line in the routine's prompt (fragile — an agent can forget it), and the failure is caught only in CI, where it presents as an unmergeable PR (see the sibling captures on the CI-only attribution gate).

This is the identity half of a pair. [[itd-91]] owns the acknowledgement half — how AI is credited (the Assisted-by: trailer style, footers, declared once per repo). Identity answers "who is the git author/committer of record" (always the human); attribution answers "how is the AI's help disclosed" (a trailer, never the identity fields). Concretely, this splits the existing check-attribution gate: its check_ident pass (the AI-name/vendor ban in author/committer fields) is itd-131's concern; its check_text pass (the trailer/footer convention) is itd-91's. The convention that the human is the author of record already exists in AGENTS.md § Attribution; this intent makes it true by construction when work starts, rather than checked after the fact in CI.

Decisions (grilled 2026-08-21)

The maintainer resolved these at the planning interview, after two adversarial reviews reshaped the draft; they are commitments, not options.

  • Detect here, establish at launch. This intent detects the wrong or tool identity (author≠committer, GIT_COMMITTER_*, an AI/tool default) and does propose-and-confirm at a TTY; establishing the human identity for a non-interactive routine is the runner's job — [[iss-2608210932052003]] for routines abcd launches. itd-131 defines the guarantee and owns detection; it does not implement the launcher. (Resolves the review's blocker.)
  • The proposal chain is disk-only: pinned → global. No gh-authenticated fallback — a network lookup in ahoy's implicit path violates [[adr-38]]. If ever wanted, it is a separate explicit verb, not this gate.
  • The five acceptance criteria are accepted as revised.

What's In Scope

  • ahoy (and doctor) extend the identity check to the committer — detecting author≠committer and GIT_COMMITTER_* overrides — reusing the env-first-then-config resolution EffectiveIdentity already applies to the author. (The author case is already covered and is not re-built here.)
  • On divergence, propose-and-confirm at a TTY: offer the pinned identity (else the global git identity) and ask before writing repo-local user.name/user.email. Never silently rewrite git config, never escalate privileges (the ahoy install posture). The proposal chain is disk-only (pinned → global) — no network lookup, per [[adr-38]].
  • Non-interactive / routine context (no TTY): the gate is fail-closed — it reports the divergence and does not write config unprompted. Establishing the human identity for a routine is the runner's job (see below), not a silent write by this gate.
  • The un-pinned repo: adopting the gate (writing identity.json) stays the ConfigChange-gated, never-under---yes step it already is.
  • Detecting the routine's tool identity: the gate recognises an AI/tool identity (e.g. the harness default) as a divergence to report, so the condition is machine-visible wherever ahoy runs. Establishing the human identity before the first commit is done by whatever launches the routine — for routines abcd itself launches, [[iss-2608210932052003]] applies it at launch; for externally-run routines, the documented env/config step until then.

What's Out of Scope

  • The acknowledgement convention (trailer style, footers, the declared per-repo preference) and its check_text enforcement — [[itd-91]] owns them; this intent cites, never duplicates.
  • Launching autonomous routines and setting their identity at launch — [[iss-2608210932052003]] owns that mechanism; this intent detects the wrong identity and defines the guarantee, but does not implement the launcher.
  • A gh-authenticated-account fallback for the proposal: a network lookup in an implicit path violates [[adr-38]] (disk-only). If wanted, it is a separate explicit verb, not part of this gate.
  • The CI-only-attribution-gate and working-tree-vs-commit gate gaps captured this session — enforcement plumbing, filed separately.
  • Rewriting history a wrong identity already produced — the gate exists so that repair is not needed; when it is, repo policy forbids force-push, so the reauthor-and-new-branch path applies (not this intent's concern).

Scope Conditions

None stated.

Acceptance Criteria

Confirmed by the maintainer at the 2026-08-21 planning interview: all five accepted as revised (after two adversarial reviews the same day).

  • Given a repo whose pinned identity is the human's and a repo-local user.name/user.email override that differs, when ahoy runs at a TTY, then it reports the divergence and proposes the pinned identity, and writes repo-local config only after the human confirms.
  • Given a commit whose committer would differ from its author (author≠ committer, or a GIT_COMMITTER_* override), when the gate checks, then the committer divergence is detected — the delta over today's author-only check.
  • Given a non-TTY / routine context, when the gate finds a divergence, then it reports and writes nothing unprompted (fail-closed) — it never silently rewrites config, and never blocks the run on a prompt that cannot be answered.
  • Given an autonomous routine running under an AI/tool identity, when ahoy checks, then it detects and reports that identity as a divergence; establishing the human identity before the first commit is verified against the runner ([[iss-2608210932052003]]), not this gate.
  • Given any divergence, when the gate acts, then it never silently rewrites git config and never escalates privileges.

Open Questions

  • How is the committer identity resolved without a throwaway commit, and without regressing StatusUnset? Extend the env-first-then-config resolution EffectiveIdentity uses to the committer fields (GIT_COMMITTER_* then committer config). NOT via git var GIT_AUTHOR_IDENT/GIT_COMMITTER_IDENT: git var fabricates an identity from gecos+hostname when none is set and exits 0, collapsing the distinct StatusUnset state the pre-commit hook blocks on — a regression the review demonstrated. Confirm the exact resolution at the grill.
  • Does splitting check-attribution (check_ident → itd-131, check_text → itd-91) need any change to the script, or only a recorded ownership note? itd-91's out-of-scope also eyes a commit/PR-time lint; the two must not both claim the same pass.

Prerequisites

  • iss-62 is the source (promoted to this intent); its Test User incident is the motivating case.
  • The routine refinement captured this session (ahoy-setup-routine-git-identity) folds in here as the detect-the-routine- identity scope.
  • [[itd-91]] is the acknowledgement-half sibling (related_intents); this intent lands consistent with itd-91's declared-preference model.

Audit Notes

Fidelity review — receipt rcp-8a6673e9bc8a (verifier intent-auditor claude-fable-5-1).

Provenance: intent-auditor@claude-fable-5-1 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:8c00566b452e24b3cfccc53a3665ee1d434077813fff058646e8b147a7313dc8 Input attestations: tree:ceb4b6dbb97622bddf2401c91f9f808ed05060a0 (origin/main; internal/core/identity/{identity,establish,toolidentity}.go, internal/core/ahoy/{detect,identity_establish}.go, internal/surface/cli/cli.go)@-;

Acceptance rollup: MET 4 · MET_WITH_CONCERNS 1 · NOT_MET 0 · INCONCLUSIVE 0

Per-criterion verdicts:

  • ac-1 — MET: an author mismatch against the pin is the required git_identity.mismatch gap; the apply step proposes the pin (else the global identity, disk only), asks Confirm at a terminal and writes repo-local user.name/user.email only on a yes; TestStepGitIdentity_ProposesPinAndWritesOnlyOnConfirm and TestStepGitIdentity_DeclineWritesNothing pass at BASE evidence: internal/core/ahoy/detect.go:336 — "ID: MismatchGapID, Category: ConfigChange, Scope: "repo"," evidence: internal/core/ahoy/identity_establish.go:76 — "if !a.prompter.Confirm("Commit to this repository as " + who + ", " + prop.From + "? (sets user.name and user.email in this repository's .git/config only)") {" evidence: internal/core/identity/establish.go:25 — "func Propose(root string) (Proposal, bool, error)" evidence: internal/core/ahoy/identity_establish_test.go:142 — "func TestStepGitIdentity_ProposesPinAndWritesOnlyOnConfirm"
  • ac-2 — MET: Check resolves the committer through EffectiveCommitter (GIT_COMMITTER_* first, then committer./user.), compares it with the author and the pin, and ahoy raises git_identity.committer; TestCheck_CommitterEnvOverrideDiverges, TestCheck_CommitterConfigDiverges and TestDetectGitIdentity_CommitterDiverges pass at BASE evidence: internal/core/identity/identity.go:371 — "committer, err := EffectiveCommitter(root)" evidence: internal/core/identity/identity.go:403 — "func committerDivergence(pin Pin, pinned bool, author, committer Effective) (bool, string)" evidence: internal/core/ahoy/detect.go:364 — "ID: CommitterGapID, Category: ConfigChange, Scope: "repo"," evidence: internal/core/identity/committer_test.go:60 — "func TestCheck_CommitterEnvOverrideDiverges"
  • ac-3 — MET: with no TerminalPrompter reporting a tty the step refuses before asking, writes nothing and records the reason, and --yes is refused the same way; the CLI's stdinPrompter reports its tty; TestStepGitIdentity_NoTerminalFailsClosed asserts no question, no config change and the refusal text, and TestAhoyInstallPipedAnswersNeverRewriteTheIdentity covers the front door evidence: internal/core/ahoy/identity_establish.go:53 — "if !atTerminal(a.prompter) {" evidence: internal/core/ahoy/identity_establish.go:49 — "if a.autoYes {" evidence: internal/surface/cli/cli.go:3782 — "func (p *stdinPrompter) AtTerminal() bool { return p.tty }" evidence: internal/core/ahoy/identity_establish_test.go:189 — "func TestStepGitIdentity_NoTerminalFailsClosed" evidence: internal/surface/cli/ahoy_identity_gate_test.go:54 — "func TestAhoyInstallPipedAnswersNeverRewriteTheIdentity"
  • ac-4 — MET_WITH_CONCERNS: detection is delivered: IsToolIdentity reads the gate's own tool-identities list plus the structural bot and noreply signals, Check sets AuthorIsTool/CommitterIsTool, and ahoy raises the required git_identity.tool gap whose hint names the runner record; concern: the criterion defers establishment to iss-2608210932052003, which is still in issues/open/, so a routine's identity is established by nothing abcd ships today and the deferral has no delivered counterpart to verify against evidence: internal/core/identity/toolidentity.go:42 — "func IsToolIdentity(role Role, name, email string) bool {" evidence: internal/core/identity/identity.go:378 — "res.AuthorIsTool = eff != (Effective{}) && IsToolIdentity(RoleAuthor, eff.Name, eff.Email)" evidence: internal/core/ahoy/detect.go:374 — "ID: ToolIdentityGapID, Category: ConfigChange, Scope: "repo"," evidence: internal/core/ahoy/identity_establish_test.go:106 — "func TestDetectGitIdentity_ToolIdentity" evidence: .abcd/work/issues/open/iss-2608210932052003-abcd-launches-autonomous-routines.md:1 — "open/"
  • ac-5 — MET: the only write is git config --local on the repository's own .git/config under a scrubbed environment, reached solely after Confirm; an outranking GIT__ override or author./committer.* key makes the step name what to unset instead of writing; TestStepGitIdentity_EnvOverrideIsNotRewritten and TestStepGitIdentity_DeclineWritesNothing pass at BASE evidence: internal/core/identity/establish.go:83 — "cmd := exec.Command("git", "-C", root, "config", "--local", kv[0], kv[1])" evidence: internal/core/ahoy/identity_establish.go:71 — "if len(over) > 0 {" evidence: internal/core/ahoy/identity_establish_test.go:248 — "func TestStepGitIdentity_EnvOverrideIsNotRewritten" evidence: internal/core/ahoy/identity_establish_test.go:169 — "func TestStepGitIdentity_DeclineWritesNothing"

Gap audit:

  • honoured:
    • detect here, establish at launch: the gate detects and proposes, and names the runner record for a routine (decision 1) evidence: internal/core/ahoy/identity_establish.go:54 — "An autonomous routine's runner sets the human identity before its first commit (" + routineRunnerRecord + ")"
    • the proposal chain is disk-only, pinned then global, no gh fallback (decision 2, adr-38) evidence: internal/core/identity/establish.go:20 — "Propose returns the identity to offer: the committed pin, else the global git // identity"
    • the committer is resolved env-first then config, not through git var, so StatusUnset survives (open question 1) evidence: internal/core/identity/identity.go:278 — "func EffectiveCommitter(root string) (Effective, error)" evidence: internal/core/identity/committer_test.go:143 — "func TestCheck_UnsetSurvivesTheCommitterPath"
    • doctor shares the detector: detectGitIdentity runs inside Detect, which every read-only mode renders evidence: internal/core/ahoy/detect.go:96 — "gaps = append(gaps, detectGitIdentity(abs)...)"
    • a forge committer (GitHub < noreply@github.com>) is not read as a tool, matching the attribution gate's role asymmetry evidence: internal/core/ahoy/identity_establish_test.go:126 — "func TestDetectGitIdentity_ForgeCommitterIsNotATool"
    • the plugin page relays the four identity gaps and the person-only answer evidence: commands/ahoy.md:169 — "(git_identity.mismatch, git_identity.unset, git_identity.committer, git_identity.tool)"
    • the un-pinned repo's pin adoption stays a prompt-only step and refuses a machine identity evidence: internal/core/ahoy/apply.go:433 — "if identity.IsToolIdentity(identity.RoleAuthor, eff.Name, eff.Email) {"
  • diverged: (none)
  • missing:

    - establishing a routine's human identity before its first commit is verified against the runner: the runner record iss-2608210932052003 is still open, so the establish half the press release promises for autonomous routines has no shipped counterpart evidence: .abcd/work/issues/open/iss-2608210932052003-abcd-launches-autonomous-routines.md:1 — "open/" evidence: internal/core/ahoy/detect.go:377 — "An autonomous routine has no one to ask: whatever launches it sets the human identity before the first commit"

Frontmatter

iditd-131
typeintent
lifecycleshipped
kindstandalone
severitymajor

References1

Referenced by4

mentions11