Development record
The managed-repo identity gate: the human is the author of record on every commit, before the first one
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 defaultClaude <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,ahoychecks 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 Useroverride authored 54 commits before anyone noticed — we had to rewrite history and force-push to unpick it," said Alice, who maintains the repo. "Nowahoycatches 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:
- The committer is never checked.
identity.Checkevaluates only the author; a committer that differs from the author, or aGIT_COMMITTER_*override, passes the disk-side gate. CI'scheck-attributiondoes 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. - The gate reports; it does not establish. Every gap's fix hint tells the
human to set
user.name/user.emailby 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. - 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 inahoy'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(anddoctor) extend the identity check to the committer — detecting author≠committer andGIT_COMMITTER_*overrides — reusing the env-first-then-config resolutionEffectiveIdentityalready 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 (theahoyinstall 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---yesstep 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
ahoyruns. 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_textenforcement — [[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.emailoverride that differs, whenahoyruns 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
ahoychecks, 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 resolutionEffectiveIdentityuses to the committer fields (GIT_COMMITTER_*then committer config). NOT viagit var GIT_AUTHOR_IDENT/GIT_COMMITTER_IDENT:git varfabricates an identity from gecos+hostname when none is set and exits 0, collapsing the distinctStatusUnsetstate 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 Userincident 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
| id | itd-131 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | standalone |
| severity | major |
.abcd/development/intents/shipped/itd-131-managed-repo-identity-gate.md
open on GitHub ↗ · commit history ↗
References1
- itd-91A Project Declares Once How It Wants AI Acknowledged — And Every Commit, PR, And Record Followsrelated
Referenced by4
- adr-2610241022541063Each person's commit identity lives on their own machine, not in the repositoryrelated
- iss-62abcd-managed repos must guarantee the user's chosen git identity (name + email) is honoured on every commit. ahoy doctor should detect when the effective git author/committer identity diverges from the user's GitHub identity — a stray repo-local user.name/user.email override (e.g. a sandbox 'Test User <test@example.com>' shadowing the global identity), or committer != author — and, rather than silently setting it, PROPOSE a default (the global git identity, or the gh-authenticated account) and ASK the user to confirm before writing repo-local config. Motivation: a repo-local 'Test User <test@example.com>' override silently authored/committed 54 commits (pushed to origin) before detection this run, forcing a history rewrite + force-push to correct. A managed-repo identity gate at ahoy/doctor time catches it before the first commit.related
- itd-2609061543533170One verb takes a managed repository's site from the checkout to a live addressrelated
- spc-34The managed-repo identity gate — detect the wrong author/committer, establish only on confirmimplemented by
mentions11
- adr-38Implicit checks are disk-only — the network answers only an explicit ask
- dcn-26092500000000282026-09-25 — The build's open-question check reads the record's own settled convention and stays fail-closed (lane implementer, autonomous run A, fix round 1 of the loop lane, on iss-2609252053576085; the lane's first ruling, that every list item under `## Open Questions` is a question whatever it says, lived only in its report and refused intents the record had settled). `intent.OpenQuestions` recognises exactly two markers, learned from the 12 planned intents that carry list items there. A section whose first non-blank line is an italic `_All resolved …_`, with at most one count word (`_All four resolved …_`), is settled whole (itd-111, itd-93); `_All but one resolved_` is not. An item explicitly marked resolved or deferred is not a question: a bold span opening with the word (`**Resolved — …**`, `**Deferred**`, `**Deferred to a follow-up intent**`, `**explicitly deferred**`, `**explicit deferral**`) or the word as a label (`resolved:`, `RESOLVED:`, `Deferred:`), anywhere in the item including its continuation lines. Everything else stays a question: an item led `**Open` whatever it goes on to say (itd-60's "Open, and not gating scope"), an item that only points to another record (itd-76's "travel with itd-126"), `**Out of scope …**`, and a question that merely mentions deferral. With the rule, itd-111 and itd-93 pass the check; itd-7, itd-60, itd-65, itd-66, itd-76, itd-82, itd-117, itd-131, itd-2609170822093401 and itd-2609211116005482 still refuse, each on an item the record does not mark settled. Relabelling those items is the product thinker's call through the planning interview, not the loop's.
- dcn-26093000000000132026-09-30 — Correcting three points of the entry above after its review (lane fix-drainOwnRule of autonomous run A). The drain-rule offer of `ahoy install` is asked only of a person at a terminal, the itd-131 precedent the git identity question set, rather than behind a named opt-in flag: off a terminal neither its category question nor the offer is asked, so a piped answer stream keeps the order it had before the offer existed and a scripted yes never writes the record, and the run reports `drain_rule.offered` under `optional_skipped` naming the terminal as the way to be asked. The terminal gate was chosen over a `--drain-rule` flag because the record decides what an unattended agent may do, which a scripted answer is not a person's yes to, and a flag would hide the offer from the person at a terminal it is for. A checkout holding no release tag (a shallow clone fetches none) marks the anchor unknown rather than reading every deferral as lapsed: every record carrying a deferral is handed back as `deferred`, naming the missing tags and `git fetch --tags`, which keeps the rest of the dry run readable where refusing the whole plan would not. The rule's reader refuses, as malformed, a record that states any frontmatter key twice (not only a `drain_` key) and one whose frontmatter `id` disagrees with its file name, and reads each record through the capped trust-boundary reader, so a record that is a symlink or past the size cap refuses; every refusal of the rule exits 2 on the dry run as on the bare verb.
- dcn-26093000000000182026-09-30 — Correcting three points of the entry above after its review (lane fix-drainOwnRule of autonomous run A). The drain-rule offer of `ahoy install` is asked only of a person at a terminal, the itd-131 precedent the git identity question set, rather than behind a named opt-in flag: off a terminal neither its category question nor the offer is asked, so a piped answer stream keeps the order it had before the offer existed and a scripted yes never writes the record, and the run reports `drain_rule.offered` under `optional_skipped` naming the terminal as the way to be asked. The terminal gate was chosen over a `--drain-rule` flag because the record decides what an unattended agent may do, which a scripted answer is not a person's yes to, and a flag would hide the offer from the person at a terminal it is for. A checkout holding no release tag (a shallow clone fetches none) marks the anchor unknown rather than reading every deferral as lapsed: every record carrying a deferral is handed back as `deferred`, naming the missing tags and `git fetch --tags`, which keeps the rest of the dry run readable where refusing the whole plan would not. The rule's reader refuses, as malformed, a record that states any frontmatter key twice (not only a `drain_` key) and one whose frontmatter `id` disagrees with its file name, and reads each record through the capped trust-boundary reader, so a record that is a symlink or past the size cap refuses; every refusal of the rule exits 2 on the dry run as on the bare verb.
- iss-85Managed attribution config: AI-disclosure attribution (prepare-commit-msg hook adding the kernel-style trailer, hooksPath wiring, AGENTS.md section) is installed by the interim prepare-this-repo skill from a private template directory. No intent covers the configuration layer owning attribution setup in managed repos; it should, so the per-repo attribution policy is declared and enforced by the tool rather than a hand-copied hook.
- iss-193A managed repo commits under whatever identity git happens to resolve, and when
- iss-2608210738367948New-repo / ahoy setup does not establish the git identity autonomous routines commit under. ahoy DETECTS a mismatch between the working git user.name/user.email and the committed identity pin (.abcd/config/identity.json, iss-62) but never SETS the identity, and nothing configures the human identity for an autonomous cloud routine's environment — so a routine defaults to Claude <noreply@anthropic.com> and every commit it makes fails the attribution gate (see attribution-gate-has-no-local-mirror). The ahoy/onboarding gap for a repo that will run routines: scaffold or verify the routine-environment git identity (git config user.name/email, or GIT_AUTHOR_*/GIT_COMMITTER_* env) so routine commits are authored by the human of record from the start. Bridges iss-62 (identity pin), itd-91 (declared AI-attribution preference), and the cloud-routine-git-identity memory. Candidate ahoy-install sub-step or a new-repo setup check.
- iss-2608210932052003abcd launches autonomous bug hunts (and other routines) for the user, opt-in — beyond handing the user a prompt to paste into a cloud routine. Today the bughunt is a Desktop prompt the user wires into an external cloud routine by hand; the direction is abcd owning the launch: the user opts in, and abcd assembles and starts the routine, applying the run contract it already governs (the human git identity per itd-131, the gates, the merge decision, the state issue). This is the realisation of the run seam — adr-27 (run is a pluggable seam, not a bespoke engine), itd-29 (the run operator surface: start/status/pause/resume/ship), itd-107 (routines assemble from one versioned template; the bughunt and a delivery-pipeline archetype), and the reframed iss-381 (the deterministic delivery pipeline survives as an itd-107 archetype, not an engine). When abcd launches the routine it sets the human git identity at launch, which is the clean mechanism the itd-131 identity gate points at for routine commits (vs today's prompt/env workaround). Big, cross-record capability — needs decomposition and likely ideate before it is filable; recorded so the direction is durable.
- iss-2609300905174086Consent gates judge /dev/null a terminal: term.IsTerminal is a character-device test, so stdin redirected from /dev/null (and a closed fd 0, which the Go runtime reopens on /dev/null) reads as a person at a terminal. The tool-install question is printed to stderr, EOF reads as no, and the decline says 'answered no at the terminal' instead of 'no terminal to ask at'; the itd-131 identity offer asks the same way, and the provider-key reader in ahoy_connect.go hand-rolls the same check and refuses /dev/null as 'stdin is a terminal'.
- itd-2610240918547728Every contributor commits as themselves
- spc-2610241007568207every-person-who-commits-to-a-repository