Development record
capture resolve refuses a record the default branch has already closed, judged by the last fetch
Typed links: related_intents itd-2609091034175565 (the claim record this was split from on the maintainer's ruling of 2026-09-09; the promotion trail from iss-2609020716570699 runs through it, and this intent carries the third part of that issue's remedy), itd-2609091416295622 (the sibling-worktree listing, split out the same day, which covers the local case as this one covers the upstream case). Prose cross-reference, not a typed link, because no schema field carries the relation (iss-2609091256264547): iss-2609020716570699, whose third clause — "capture resolve refusing a record that is already terminal on the fetched origin/main" — this intent is, with the fetch withdrawn.
Press Release
capture resolvesays no when the default branch has already closed the record, before the push gate has to. When a session resolves an issue that the localorigin/mainref — as of the last fetch — already holds underresolved/orwontfix/, the verb refuses, names the terminal path at that ref and the commit that put it there, states how old the ref is, and writes nothing.capture wontfixrefuses the same way. The verb performs no fetch: what it knows is what the last fetch brought, and it says when that was, so a stale answer is a legible one. The same judgement is rendered read-only earlier, where it costs nothing:abcd <record-id>on an open issue says, before the next moves, that the default branch already holds it terminal, so a session that looks before it fixes learns it before the fix and not after. Nothing about the claim, the lease or presence is involved: this is a comparison of two trees git already holds."Two of my open pull requests turned out to duplicate work that had merged the day before, and the first thing that told me was the resolution gate refusing the push," said Maya, an autonomous-development practitioner who runs agent sessions against a shared default branch. "The ref that proved it had been sitting in my checkout since the morning fetch. Now the resolve refuses on the spot and tells me how stale my view is, and the record's own page told me first."
Why This Matters
On the night of 2026-09-01, two open pull requests from one session duplicated work already merged to the default branch, and the resolution gate — scripts/check-issue-resolution.sh, rule RS001 — refused the push, correctly, after the fix was written, tested and reviewed (iss-2609020716570699). RS001 already tells the stale-branch shape apart from a record left open or missing: it names a record that is terminal at the base as the case where a rebase is the remedy and "resolve it" is not (iss-2609012023256534). The shape is a comparison between the ledger at the head and the ledger at the base, and the base is a ref the checkout holds locally from its last fetch. Every input to the judgement was on disk when the session started the fix; only the moment of asking was wrong.
The source issue proposed this refusal as the third of three remote-mediated parts, judged "on the fetched origin/main". The claim record it was promoted into corrected the fetch away — a read-adjacent verb must not do surprising network I/O, and a fetch moves refs under the feet of every peer sharing the checkout — and kept the refusal inside a design that also carried a lease, a stamp and a pushed duplicate guard. The two adversarial reviews of 2026-09-09 found the claim's parts unsound as drafted and this part independent of all of them, and the maintainer ruled it its own record. It stands alone: no claim is consulted, no lease is read, nothing is written that is not written today.
One thing is said rather than implied. A refusal at capture resolve lands after the fix is written, because AGENTS.md requires the resolve in the same change as the fix; what it saves is the commit, the push, the CI minutes and the review, not the fix. The earlier moment is the read-only render: the same comparison on abcd <record-id>, which is the surface an agent consults for its next move before it starts one. Both surfaces are in scope so that the answer is available before the work for a session that looks, and enforced after it for one that does not.
What's In Scope
capture resolve <iss-N>andcapture wontfix <iss-N>refuse a record terminal at the local default-branch ref. The verb reads the ledger atrefs/remotes/origin/mainthrough git's object store —git ls-treeover the two terminal folders, by id in the filename, the test RS001 makes — and refuses when the id is present under either. The refusal names the terminal path at the ref, the ref's commit, and the ref's age; the record stays where it is, no grounds are appended, and nothing is written.- The ref's age is stated, and no fetch is performed. The refusal and the render say when the ref was last brought up to date, from what git records locally; which timestamp is the honest one is an open question below. A checkout with no
origin/mainref — never fetched, or no remote — proceeds exactly as today, and says in one line that no upstream ref was available to judge by. abcd <record-id>on an open issue renders the same judgement, read-only, beforenext_moves: terminal atorigin/mainas of the stated age, with the terminal path. A record open at the ref renders as today.- The two judgements agree by construction. RS001 is shell and cannot import Go, so the two spellings of "terminal at the base" are pinned to each other by a test that runs both over one fixture, the way the ledger's status-folder list is already pinned between
issueschema.StatusDirsand the shell gate. --jsoncarries the ref, its commit, its age and the terminal path in the refusal and in the render.
What's Out of Scope
- Fetching. The verb never touches the network. Bringing the ref up to date is the user's
git fetch, and the age line is what tells them to. - The claim, the lease, and any refusal that names a peer session. Those are the claim record's, with their open questions.
- A record terminal in a sibling worktree but not on the default branch, which the sibling-worktree listing renders.
- Refusing
capture promoteorintent planon the same ground. Promoting a record the default branch has closed is a different act with a different remedy; whether it should refuse is an open question below, not a scope item. - Any change to RS001. The push gate stays, unchanged, as the last line; this intent moves its answer earlier and leaves it in place.
Mechanism
We expect a local comparison against the last-fetched default-branch ref to catch the merged-work duplicate before the push because in the case on record every input to RS001's judgement was already in the checkout when the fix began — the ref had been fetched and the ledger at that ref held the record terminal — and RS001 proves that the comparison classifies the shape correctly. It is falsified where the ref is older than the upstream close: the record went terminal on the default branch after the last fetch, the verb passes, and RS001 still refuses at the push; the age line makes that case legible in hindsight, and if it turns out to be the common case rather than the rare one, the answer is not a fetch inside the verb but a fetch the session is told to run.
Scope Conditions
- Holds only as fresh as the last fetch. A record closed on the default branch after the ref was last updated is not seen here and is caught by RS001 at the push, as today; the age line is the disclosure that makes the miss legible.
- Holds where the checkout has an
origin/mainref. A checkout that has never fetched, has no remote, or names its remote or default branch otherwise proceeds as today and says the check did not run; what to do about a repository whose default branch is notorigin/mainis an open question, not a silent assumption. - Holds for the ledger as the default branch commits it: terminal means a file whose name carries the id under
resolved/orwontfix/at the ref, the same test RS001 makes. A record absent at the ref is not terminal; a record open at the ref is not terminal. - Reads the ref through git's object store, so the judgement is the same whatever branch is checked out, whatever the working tree holds, and whatever is stashed; and it is the same under merge, squash and rebase merges, because it compares trees and not ancestry.
- Holds for repositories where the resolve is the last step of the work, which
AGENTS.mdrequires here. The refusal saves the commit, the push, the CI run and the review, never the fix; the fix is saved only by a session that consults the render first. - Holds offline. The outcome with the network unplugged is byte-identical to the outcome with it connected.
Acceptance Criteria
- Given a record open in this tree and present under
resolved/at the localorigin/mainref, whencapture resolve <iss-N>runs, then it refuses, names the terminal path at the ref, the ref's commit and its age, the record remains underopen/byte-identical, no grounds entry is appended, and the exit code is non-zero. - Given the same record, when
capture wontfix <iss-N>runs, then it refuses the same way. - Given a record open both here and at the ref, when
capture resolveruns, then it proceeds exactly as today, and its output carries no upstream line. - Given a checkout with no
origin/mainref, whencapture resolveruns, then it proceeds as today and says in one line that no upstream ref was available to judge by. - Given the network unavailable, when
capture resolveruns against a record terminal at the ref, then the refusal is byte-identical to the refusal with the network available, and no fetch is attempted — the test runs with the remote URL pointed at a path that does not exist. - Given a record terminal at the ref, when
abcd <iss-N>runs on the open copy, then the render says the default branch holds it terminal, names the terminal path and the ref's age, beforenext_moves, and offers no resolve move. - Given one fixture repository in which a record is terminal at the base and open at the head, when RS001 and the verb's judgement both run over it, then both classify it as terminal at the base, and the test fails if either disagrees.
- Given
--json, when the refusal or the render fires, then the payload carries the ref, its commit, its age and the terminal path. - Given a record whose id appears under
resolved/at the ref only in a filename with a different slug, when the verb runs, then it is judged terminal, because the id and not the slug is the record's identity.
Prior Art
scripts/check-issue-resolution.sh— RS001, which already tells "terminal at the base" apart from the other three shapes at push time (iss-2609012023256534); the classification this intent moves earlier and leaves in place.- iss-2609020716570699 — the source: its third part is this refusal, proposed with a fetch; the fetch was withdrawn in the claim record and stays withdrawn here.
internal/gitutil—Run,IsAncestor, the scrubbed environment; the ledger at a ref is onels-treethrough it.internal/core/issueschema.StatusDirsand its shell pin — the precedent for holding a Go spelling and a shell spelling of one ledger fact to each other by test.AGENTS.md§ Definition of done — the rule that the resolve lands in the same change as the fix, which is why the refusal is after the fix and the render is the earlier surface.
SOTA
Checked on 2026-10-23 against git's own manual (git 2.52.0, git help config,
the version on the development machine). The design is otherwise internal: the
ledger at a ref, RS001's classification and the Go/shell pin are all
repository mechanics, listed under ## Prior Art. The two open questions
below that turn on git's documented behaviour have an answer in the manual.
- "Which age to state" has a third source beside the two the record names,
and it answers a different question. With
core.logAllRefUpdates, each update to a ref is logged "by appending the new and old SHA-1, the date/time and the reason of the update". The log is created automatically for remote refs underrefs/remotes/. This is "true by default in a repository that has a working directory", false in a bare one. Sogit reflog show --date=iso origin/maingives the time the local copy of the default branch last moved, and why ("fetch: fast-forward"). A ref is logged only when it changes: a fetch that finds the default branch unmoved writes no entry, so on a quiet upstream the reflog age grows while the view is fresh, overstating staleness.FETCH_HEAD's mtime has the opposite flaw: it says when we last looked, at anything. The two answer different questions, "when did our copy last change" and "when did we last look", and reflogs expire (90 days by default). One line can state both with their meaning, falling back where no reflog exists (a bare repository, or one with the setting off); which line is the interview's, not this pass's. - The default branch can come from git, not a new identity field.
remote.<name>.followRemoteHEADdefaults tocreate, sogit fetchcreatesremotes/<name>/HEADwhen the remote has one and the local copy does not. An already existing local reference is left untouched unless the setting isalways;warnreports a mismatch. A repository fetched by a recent git therefore carriesrefs/remotes/origin/HEAD. A stale symbolic ref is the failure mode, andwarnis the documented way to see it. - Both sources are read-only and need no fetch, which keeps the record's withdrawn-fetch ruling intact.
Path 1 for the two facts (adopt git's reflog and remotes/<name>/HEAD as
they are); the refusal itself stays native. No dependency. The independent
fit-challenge runs at plan time.
Open Questions
- Which age to state. Git records no per-ref fetch time; what it has is the modification time of
FETCH_HEAD(the last fetch of anything) and the ref tip's commit date (when the upstream last changed, not when we last looked). Stating both is honest and noisy; the interview picks the line. - The remote and branch names.
origin/mainis this repository's convention. Whether a managed repository declares its default branch in.abcd/config/identity.json, or the verb asks git forrefs/remotes/origin/HEAD, is the interview's; until then a repository without the ref gets the as-today path and the one-line notice. - Whether
capture promoterefuses on the same ground. Promoting a record the default branch has closed produces an intent from a finding already answered; the remedy differs from a resolve's, and a warning may fit better than a refusal. - Whether the refusal is overridable. A record deliberately re-opened locally after an upstream close is conceivable; no case is on record, so no flag is proposed.
- Impact. Additive on its face — a new refusal and a new line, no existing invocation changed; left unset until the interview judges it.
Audit Notes
Empty. Populated by intent-auditor when intent moves to shipped/.
Frontmatter
| id | itd-2609091416304128 |
|---|---|
| type | intent |
| lifecycle | drafts |
| severity | major |
.abcd/development/intents/drafts/itd-2609091416304128-capture-resolve-refuses-a-record-the.md
open on GitHub ↗ · commit history ↗
Referenced by4
- iss-2609020716570699Nothing tells an agent that a record it is about to fix has been claimed or resolved by another session until the resolution gate refuses the push. In one night a peer session re-fixed two issues a paused branch also fixed, and two of its open PRs duplicate merged work. The claim signal that worked in every published multi-agent run is the repository itself: a claim written into the open record (claimed_by: account and harness, branch) and pushed alone through the queue before any fix starts, so a losing race is a push rejection; plus a duplicate-guard required check that fails a PR whose Resolves trailer names a record already resolved on origin/main; plus capture resolve refusing a record that is already terminal on the fetched origin/main. Folder membership is already the status signal, so the claim extends the one canonical primitive rather than adding a lock file that rots. Refines iss-2608220750029993.related
- iss-2610262037219105itd-2609091416304128 treats 'the default branch' as refs/remotes/origin/main; 34-build defines it as origin's HEAD, falling back to main, master, trunk, then develop.related
- itd-2609091034175565Before an agent starts on an item, it reserves it on the shared remote, and everyone who works on the project sees the reservationrelated
- itd-2609091416295622A session sees the records its peers hold before it mints or fixes onerelated
mentions4
- dcn-26090900000000052026-09-09 — itd-2609091034175565, the claim intent, is split into three records (maintainer, ruling after two adversarial reviews of the widened draft, design/feasibility and record-discipline). The reviews found the expensive parts unsound as drafted: a `claimed_by` stamp on an issue record is invisible to any peer on an older binary, because `issueschema.Known` is a closed allow-list whose reader refuses and skips a record carrying an unknown key and `record_schema` mirrors the refusal into the gate, and version skew is the steady state here (five plugin-cache vintages beside a `go run` checkout); the write-verb refusals fire after the fix is written, because `AGENTS.md` puts `capture resolve` in the same change as the fix; no staleness threshold is safe in both directions for two sessions in one worktree, since the worktree-exists and branch-merged tests are identical for both and a host may fire session-end on a context clear with the human still present; and the pushed half costs a merge-queue pass per claim, measured at fifteen to sixteen minutes on this repository's merge-group `ci` leg. Every collision on record is one of two claim-free shapes. The ruling: itd-2609091416295622 carries the read-only sibling-worktree ledger diff over `git worktree list --porcelain` (no claim, no lease, no hook, no threshold; the piece that ships soonest); itd-2609091416304128 carries `capture resolve` and `capture wontfix` refusing a record terminal at the local `origin/main` ref as last fetched, stating the ref's age and fetching nothing, with the same judgement rendered read-only on `abcd <record-id>`; itd-2609091034175565 keeps the claim, the lease and the refusals as a draft marked not ready, carrying the refusal-surface, liveness and pushed-price questions as open questions and the two-release stamp migration as a hard constraint. The existing record stays the claim so the `promoted_from` trail from iss-2609020716570699 — whose remedy is the claim and the pushed half — stays true rather than being retitled onto a listing the issue never proposed; the two new records reach the issue through `related_intents` on the issue and on themselves, and every relation the schema cannot type is stated in prose as a prose cross-reference (iss-2609091256264547). All three sit in `drafts/`; adoption is the interview's. Rejected: re-scoping the existing record to the listing, which keeps the trail mechanically and breaks it semantically; a fifth relation word for "split from", which is the vague form the decomposition principle forbids.
- iss-2609012023256534RS001 in scripts/check-issue-resolution.sh cannot tell a stale branch from a forgotten resolution, and its message diagnoses the wrong one. On a branch 235 commits behind origin/main the pre-push preflight emitted 84 RS001 violations, every one instructing the reader to resolve an issue that already sat in a terminal folder at the base: the branch's own commits had been squash- or rebase-merged, so origin/main held the moved records, the two-dot diff of the two trees showed no record entering resolved/ or wontfix/, and the trailers went unsatisfied within the range. No line of the output said the branch was behind, and the one remedy that works, a rebase, appeared nowhere. The gate should distinguish the shapes it can prove: a record already terminal at the base ref (with how far behind the head is and which base-side commit placed it there), a record absent from the head tree while the base holds it, and an id with no record anywhere, and name a rebase as the likely remedy where the evidence points to one. Carried from the session handover into autonomous-run-2026-09-01.
- iss-2609091256264547The decomposition discipline requires a cross-record link to be typed with one of four relations, supersedes or reverses or duplicates or refines, and never a vague related. Only the first of those four exists. supersedes is schema-known through recordHandleFields and is carried by fifty-eight decision records and three others, while reverses, duplicates and refines appear in no schema and in no committed record anywhere in the tree: the field list a record may carry is related_adrs, related_intents, builds_on and blocked_by, and a record carrying a key outside the known set is dropped by the reader rather than reported, so writing one of the three missing words would make the record invisible to every surface that reads it. The consequence is that an author following the rule literally either writes a link the tooling silently discards or writes the relation in prose and calls it typed, and the corpus shows the second: records assert a typed link in a heading and carry no frontmatter edge, which reads as done to a human and is invisible to every graph walker. That is the shape enforcement-claims-are-facts refuses, a convention naming a mechanism that does not exist, and it is load-bearing here because the decomposition protocol is the documented gate until its automated rung ships. Fix candidates, none chosen: implement the three missing relations as known fields so the rule can be followed; or narrow the rule to the vocabulary the schema implements and say what an author does with a relation the four words cannot express; or record that prose is the sanctioned form for the three and stop calling them typed. Detector: a record asserting a typed relation in its body carries a frontmatter edge naming the same target, and a relation the schema cannot express is refused at write time rather than dropped in silence.
- spc-2610031753359849Reservations on the shared remote: `abcd reserve`, one liveness judgement, and the build, drain and land integration