Development record
A gate catches the intent whose work is live while its spec is still open
Typed links: promoted from iss-2609091642508005, which states this remedy in
its own words and carries the measured population. The verbs this record first
proposed are held until the gate has run a cycle, on the maintainer's ruling of
2026-09-11.
Press Release
An open-source maintainer lands the work an intent promised, opens the pull
request, and forgets the one step nothing runs for them: closing the spec. Today
that intent stays in planned/ indefinitely. It ships with no changelog line,
the release exits 0, and nobody finds out until somebody reads.
With this gate they find out while the change is still in their hands. The commit already says which intent it delivers, the way it already says which issue it resolves; the gate reads that line and refuses to let the change land with the intent still planned, naming the command that closes it.
"I do not need a new verb. I need to be told, at the moment it matters, that I forgot the one I already had," said Kira, an open-source maintainer.
Why This Matters
The population is measured, not estimated: 61 planned intents, every one with
its spec still open (iss-2609091642508005). At least one is a confirmed live
instance — itd-121's record-id dispatch has been shipping behaviour while
spc-26 sat in open/, and it was found by a person reading, not by a gate.
Half of this hole is already closed, which is what makes the other half worth
closing. release/emit.go:294-332 (staleIntents, RefusalStaleIntent) refuses
a cut carrying a planned intent whose spec has closed, naming each one. The
symmetric case — spec still open — is invisible, and it is the more common
one, because closing the spec is exactly the step that gets forgotten.
This record was filed proposing verbs instead, and two earlier rulings had
already declined them. iss-2609011026401952 resolved the same complaint with
"No new verb was needed", and left the alternative open in its grounds: "a
mechanical gate that refuses a merged intent whose spec is still open would be
the durable fix". iss-2609091642508005 then measured the population and said
what is owed is "the detector, not another audit by hand". The spc-26 case
argues the same thing from the other end: the verb existed, was one command
away, and went unrun. A second verb is a second verb nobody runs.
Prior Art
iss-2609091642508005(open,major, deferred after v0.7.1) — the record this is promoted from. States the remedy, carries the measured population of 61 and the two confirmed instances.iss-2609011026401952(resolved) — the same complaint resolved against a verb, naming the mechanical gate as the durable fix. This record builds what that ruling left open rather than reversing it.release/emit.go:294-332—staleIntentsandRefusalStaleIntent: the closed-spec half of this gate, already shipped. The pattern to follow, and the reason the new refusal should read like an existing one rather than a new dialect.internal/core/lint/lint.go:2199-2252—checkSpecLifecyclechecks id, slug, link existence and bidirectional agreement, and never bucket coherence between the two stores. The nearest existing home, and evidence the check is absent rather than merely weak.itd-34(planned) — owns intent-kind taxonomy and the supersede path. Not related to this record; named here because an earlier revision wrongly absorbed its supersession clause, and the absorption was reverted.- The retirement verbs —
intent shipand the intent supersede path remain unowned and unfiled. Held deliberately: the maintainer ruled on 2026-09-11 that the gate runs for a cycle first, so that what people actually get wrong is measured before a verb is designed for it.
What's In Scope
- A mechanical refusal for the state: intent in
planned/, its work live, its spec stillopen/. - A refusal that reads like
RefusalStaleIntentand names every offending record and the command that fixes each. - The signal, settled at interview: a commit that delivers an intent SAYS SO in
its trailer, and the gate refuses when the named intent is not shipped in the
same change. No liveness is inferred; the author declares it. This is the shape
Resolves: iss-Nand RS001 already have for issues.
What's Out of Scope
intent shipand the intent supersede verb. Held for a cycle by ruling.- An ADR supersede verb. Five lifetime invocations, two legal dispositions
one of which prunes the file, no directory move, and
schema.go:606-645already refuses a one-way edge as a blocker. - Unifying the retirement vocabulary. The four words carry distinctions that
a shared vocabulary would flatten, and the per-family impact vocabularies
differ on purpose (
intent/lifecycle.go:691-700). - Backfilling the 61. Deciding what to do with the existing population is its own act; this record makes the state visible.
Mechanism
We expect a refusal to close this gap because the remedy it replaces was documentation, and documentation cannot refuse. The 61 records accumulated under a rule every contributor had already read, stated in the definition of done and repeated on the surface pages; the thing that was missing was never the wording. A gate that stops the change is a different kind of object from a rule that describes it.
What would show this wrong: authors omitting the trailer in order to avoid the refusal. That would make the gate a thing to route around rather than a thing that helps, and it would show the cause was not forgetting but reluctance — in which case the remedy is somewhere else entirely, because a gate keyed on a voluntary declaration cannot reach someone who declines to declare.
Scope Conditions
- The people using abcd mean to keep the convention and forget the last step, rather than skipping it on purpose. The gate reads a line the author chose to write, so it only ever helps the willing: a larger contributor base, or one that finds the rule irksome, simply omits the line and the gate never fires.
- The refusal reaches the author while the change is still theirs, before it merges. The same check at release time would interrupt whoever happens to be cutting the release about somebody else's forgotten step, days late — the same rule at a moment where acting on it is somebody else's work.
- A piece of work is finished by a single change. Where an intent arrives across several pull requests, no one change can honestly say it finishes it, so the gate either objects to the first partial delivery or teaches authors to stop writing the line.
- Closing a spec stays one quick command with no ceremony. If it later needs a review sign-off or an audit first, being told "you forgot to close it" blocks the author on somebody else's availability, and avoiding the trailer becomes cheaper than triggering the check.
Acceptance Criteria
Confirmed by the maintainer at the planning interview of 2026-09-11. The set was walked bullet by bullet; five were accepted as written and the fifth was rewritten on the maintainer's instruction, because "reads as one convention" was a review judgement wearing Given-When-Then clothes and could not fail.
- Given a change whose commit declares that it delivers an intent, when
that intent does not reach
shipped/in the same change, then the gate refuses and names the command that closes its spec. - Given a change that declares delivery of several intents, when any one of them stays planned, then every unshipped one is named, so the author clears them in one pass rather than one refusal at a time.
- Given a change that declares no delivery, when it lands, then nothing is refused, because a planned intent nobody claims to have built is the ordinary state of the backlog.
- Given a change that declares delivery of an intent that does not exist, or one already shipped, when the gate runs, then it says which, rather than passing silently or refusing without naming the cause.
- Given the existing issue rule's refusal, when the intent rule refuses, then it uses the same message shape and the same exit code, and a test compares the two rather than a reviewer judging that they feel alike.
- Given the 61 records already planned with open specs, when the gate arms, then none of them is refused by it, because none declares a trailer — and the record says plainly that clearing them is a separate act.
Open Questions
- RESOLVED at interview 2026-09-11: the signal is a commit trailer. Nothing in a record says an intent's work has landed — specs carry no task checkboxes and intents carry no delivering-commit stamp — so no liveness can honestly be inferred. The author declares it instead, and the gate enforces the declaration. Open underneath it: the trailer's spelling, and whether it takes the intent id or the spec id.
- What the trailer gate does NOT catch, and whether that is acceptable. It fires only on a change that declares the trailer. A change that delivers an intent silently is invisible to it, and so are the 61 records already in this state — which arrived there by nobody saying anything, which is the same absence. The issue rule accepts exactly this limit ("Resolving without a trailer stays legal"), and the argument for accepting it here is the same: the trailer is cheap to write while describing the change, and the gate converts a description a person was going to write anyway into an enforced close. The argument against is that the measured problem is silence, and a gate keyed on speech does not reach it.
- Where does the gate live? The trailer decides this largely: RS001 runs at
merge time in
lint-issues, so the symmetric rule belongs beside it rather than at the cut.staleIntentsstays where it is and keeps the closed-spec half at release time. - What happens to the 61? The trailer gate does not reach them, so they are
not a wedge risk — and they are also not fixed. Clearing them is a separate
act, and this record should say whether it owns it or hands it back to
iss-2609091642508005. - Is a refused release the right severity? A merge-time finding is cheaper to act on and harder to ignore; a release-time refusal is louder and later.
Audit Notes
None yet: the intent has not shipped.
Fidelity review — receipt rcp-600cc27b0c43 (verifier intent-auditor claude-fable-5-1).
Provenance: intent-auditor@claude-fable-5-1 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:eb995a6229933334408bd589e5d78f4bf0f3a96f316cc1a0d3bee7ad482e769d Input attestations: diff:8486c141..97e8ce1a (PR #667, merged 0c0ca52d; tree read at cede78b8)@-;
Acceptance rollup: MET 6 · MET_WITH_CONCERNS 0 · NOT_MET 0 · INCONCLUSIVE 0
Per-criterion verdicts:
- ac-1 — MET: a Delivers: id absent from the set entering shipped/ is a refusal, and the planned-with-open-spec branch names
abcd spec close <spc-N>for each open spec; the cases script asserts the refusal text down to the close command evidence: scripts/check-issue-resolution.sh:415 — "printf '%s\n' "$shipped" | grep -qx "$id" && return 0" evidence: scripts/check-issue-resolution.sh:451 — "Close them in this change ($cmds)" evidence: scripts/check-issue-resolution-cases.sh:792 — "expect_refusal_naming "$d" "RS005 trailer, intent left in planned/ with its spec open"" - ac-2 — MET: every id on every Delivers: line is judged by check_delivery in turn, and the cases assert three unshipped intents across two trailer lines are each named and counted as their own violation evidence: scripts/check-issue-resolution.sh:586 — "check_delivery "$sha" "$cid" "$base" "$head" "$shipped" "$behind"" evidence: scripts/check-issue-resolution-cases.sh:826 — "Delivers: itd-7, itd-8" evidence: scripts/check-issue-resolution-cases.sh:834 — "expect_refusal_naming "$d" "RS005 counts every unshipped intent as its own violation""
- ac-3 — MET: the rule is keyed on the trailer, and a case commits an edit to a planned record with open specs all around and no trailer, expecting a pass evidence: scripts/check-issue-resolution.sh:216 — "change that declares no delivery is refused nothing (the intent's criterion" evidence: scripts/check-issue-resolution-cases.sh:844 — "expect pass "$d" "RS005 no trailer, planned intents with open specs are not refused""
- ac-4 — MET: an id with no record at head or base, one already in shipped/ before the branch, and one shipped on the base side after divergence are three distinct refusals, each case-held and each asserted not to prescribe the wrong remedy evidence: scripts/check-issue-resolution.sh:425 — "fail "$says $id has no record at $head or at $base." evidence: scripts/check-issue-resolution.sh:436 — "the trailer names an intent delivered before this commit. Drop the trailer."" evidence: scripts/check-issue-resolution-cases.sh:853 — "expect_refusal_naming "$d" "RS005 on an id with no record anywhere says so"" evidence: scripts/check-issue-resolution-cases.sh:864 — "expect_refusal_naming "$d" "RS005 on an intent already shipped before the branch says to drop the trailer""
- ac-5 — MET: RS005 reuses RS001's fail helper and exit path, and the cases script normalises one RS001 and one RS005 refusal through a single pattern and requires them byte-identical, exit code included evidence: scripts/check-issue-resolution.sh:224 — "fail() {" evidence: scripts/check-issue-resolution-cases.sh:1042 — "# Criterion 5: the intent rule's refusal has the issue rule's shape and exit" evidence: scripts/check-issue-resolution-cases.sh:1068 — "if [ "$shape_iss" != "$shape_itd" ]; then" evidence: scripts/check-issue-resolution-cases.sh:1071 — "grep -qx 'exit=1'"
- ac-6 — MET: the gate is armed in CI and preflight over the PR range, fires only on a trailer, and the record states in both the intent and the spec that clearing the standing backlog is a separate act handed to its own issue evidence: .github/workflows/ci.yml:473 — "bash scripts/check-issue-resolution.sh commits "$BASE_SHA" HEAD" evidence: Makefile:183 — "@bash scripts/check-issue-resolution.sh commits origin/main HEAD" evidence: scripts/check-issue-resolution-cases.sh:837 — "# Criterion 3 and 6: no trailer, no refusal — even with planned intents whose" evidence: .abcd/development/specs/closed/spc-2609120450289528-a-gate-catches-the-intent-whose-work-is.md:29 — "Clearing the standing 61, which this gate"
Gap audit:
- honoured:
- one trailer, one rule, beside RS001 at merge time evidence: scripts/check-issue-resolution.sh:208 — "DELIVERS_RE='^Delivers:[[:space:]]+itd-[0-9]+([[:space:]],[[:space:]]itd-[0-9]+)[[:space:]]$'" evidence: .github/workflows/ci.yml:465 — "Declared resolutions and deliveries move the record, mentions declare themselves (RS001/RS002/RS004/RS005)"
- a planned intent with spec_id: null is told it has no spec to close rather than a command that cannot run evidence: scripts/check-issue-resolution.sh:457 — "with no spec to close (spec_id: null, and no open spec names it), so no close can ship it"
- the trailer means the change finishes the intent, and the contributor guide says so
evidence: CONTRIBUTING.md:67 — "
Delivers: itd-Nfor an intent the change finishes — not one it" - the gate reads the intent and spec stores from declared paths evidence: internal/core/lint/preflightgates_test.go:340 — "func TestIssueResolutionGateReadsTheIntentAndSpecStores"
- diverged:
- the trailer is matched exactly as RS001 matches its own: delivered wider, refusing a near-miss spelling that carries an id-shaped token rather than passing it over evidence: scripts/check-issue-resolution.sh:579 — "carries a delivery line RS005 cannot read" evidence: scripts/check-issue-resolution-cases.sh:994 — "for spelling in "Delivers: spc-7" "delivers: itd-7" "Delivers: itd-7 and itd-8"; do"
- the ordinary refusal names the intent's spec_id: delivered as every open spec whose back-link names the intent, per the 1:n rule evidence: scripts/check-issue-resolution.sh:451 — "with $n spec(s) still open that name it ($list)" evidence: scripts/check-issue-resolution-cases.sh:946 — "expect_refusal_naming "$d" "RS005 names the remainder spec still open""
- missing: (none)
Scope-condition dispositions:
- cond-2609120450285738 — untested: whether authors write the trailer or omit it to dodge the gate is what the cycle after arming measures; nothing in the delivered diff exercises it
- cond-2609120450285806 — survived: the rule runs in the record-lint job on the pull request's base sha and on the merge-queue entry, and in preflight against origin/main, so the refusal reaches the author before the merge evidence: .github/workflows/ci.yml:467 — "BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }}" evidence: Makefile:183 — "@bash scripts/check-issue-resolution.sh commits origin/main HEAD"
- cond-2609120450285032 — survived: the trailer is defined as the finishing change's, the guide tells a multi-PR author to write it on the last one, and the gate judges the whole range so a close in a later commit of the same change passes evidence: CONTRIBUTING.md:68 — "contributes to, since an intent that arrives across several pull requests is" evidence: scripts/check-issue-resolution-cases.sh:815 — "expect pass "$d" "RS005 trailer with the close in a later commit of the range""
- cond-2609120450283625 — survived: closing a spec is still one command with no sign-off, and the refusal names exactly that command evidence: commands/intent.md:378 — "spec close < spc-N> --json # open/ -> closed/, and planned/ -> shipped/ when it was the last open spec" evidence: scripts/check-issue-resolution.sh:475 — "Close its spec in this change (abcd spec close < spc-N>) or drop the trailer."
Grounds
- pursued: we expect the gate to measure what people actually get wrong, and that measurement is what decides whether the retirement verbs are worth building at all — which is why it was chosen ahead of them rather than beside them. What would show this wrong is a cycle of refusals that cluster on something neither the gate nor a verb addresses, meaning the forgotten close was a symptom and we treated it as the disease.
Frontmatter
| id | itd-2609111003026787 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | standalone |
| severity | major |
.abcd/development/intents/shipped/itd-2609111003026787-a-gate-catches-the-intent-whose-work-is.md
open on GitHub ↗ · commit history ↗
Referenced by3
- iss-2609091642508005The agent-contract prompt lint ships and is armed as a blocker in the record-lint configuration, enforcing the frontmatter floor on every prompt under the agents directory and gating the diff-armed unbumped-edit check, yet the intent that promised it sits in the planned bucket and its spec sits open. The definition of done says a change that delivers a planned intent closes its spec in the same change, and that the omission is silent by construction: the release cut composes its changelog from terminal folders only, so an intent whose code is on the branch with its spec still open ships with no line and the cut exits zero doing so. That is the state here, and it means the release notes for this cut describe a repository in which this rule does not exist. The condition was found by a cross-check of the design record against the shipped surface rather than by any gate, because no gate asks whether a planned intent's work is already present: the record lint reads what the records say about each other and the release derivation reads only what reached a terminal folder, so the one question that would catch it, whether the code a planned intent describes is already merged, is asked by nothing. Fix: close the spec so the intent ships and the cut sees it, once the acceptance criteria are checked against what actually landed, which is the maintainer's call rather than an automatic move. Detector: a planned intent whose acceptance criteria are satisfied by code already on the branch is reported before a release derives its cut, so the choice to ship it or to keep it planned is made rather than defaulted.related
- iss-2610262045552105itd-46 lists ship as a live intent sub-verb; itd-2609111003026787 holds intent ship for a cyclerelated
- spc-2609120450289528A gate catches the intent whose work is live while its spec is still openimplemented by
mentions12
- dcn-26091500000000072026-09-15 — The 1:n intent–spec rule of adr-2609151513118583 is built, and the spec's `intent:` back-link is the single source of truth for which specs realise an intent (implementing session, ruling on iss-2609100508566552's build, reversible by the product thinker): the intent's `spec_id` stays a scalar meaning the spec it was planned with, the set is derived by one canonical comparison shared by the spec store, the intent corpus and the record-lint index (a zero-padded back-link once let one reader ship an intent the other saw as still open), and the bidirectional check becomes membership. `spec close <spc> --remainder <slug>` mints the follow-on spec before any move and reuses an open one with that slug on retry; `--remainder` is refused on a shipped intent or a closed spec; `--impact` at a close that does not ship is refused rather than ignored, because the flag's only effect is the judgement `shipped/` requires. The fidelity audit request names every spec that realised the intent and the receipt stays owed once per intent. Noted for the gate itd-2609111003026787 plans on the peer's branch, a commit declaring delivery of an intent must move it to shipped/: under this rule a change may deliver one spec and leave the intent planned with a remainder open, so that gate's condition should read "no open spec remains" when it is built. Rejected: an intent-side list of spec ids (a second writable carrier that can disagree with the store, and eighty-five records to edit); a new verb for the remainder (the close is the moment the remainder is known).
- dcn-26092300000000162026-09-23 — itd-4 AC3 built as ruled (B3): the promote join's back-links are `related_issues` on the intent and `related_intents` on the ledger record, and `abcd intent audit --issue-drift` checks them. Seven implementation decisions the ruling left open, taken by the implementing lane. (1) No reader tolerates the retired `promoted_to` / `promoted_from`: the ledger reader refuses and skips a record carrying `promoted_to`, the intent reader ignores `promoted_from`, and a promote write meeting either refuses. The refusal, the record gate's finding and the drift finding each name the successor and the remedy, `abcd capture migrate --apply`, a verb that reports by default, writes only with `--apply`, and is idempotent. It is a verb and not a one-off script because managed repositories hold the same retired names and need the same repair. (2) "Promoted" is the PAIR, an intent in the record's `related_intents` that names the record back in its `related_issues`, because an issue's `related_intents` already carried loose relations before the rename and still does; the double-promote refusal and the record page's next move read the pair. So the migration completes a join an older abcd wrote from one end only (iss-327 and itd-93, iss-2609091642508005 and itd-2609111003026787), or those records would stop reading as promoted. (3) Link mode on the issue route writes both halves; before the rename it stamped the issue alone, which left a hand-filed draft with no edge back. (4) `related_issues` holds `rdi-N` as well as `iss-N`: a reading item lives in the issue ledger's tree and graduates the same way, and one key per side keeps the join one shape. The first entry is the record the intent was promoted from; a later link appends, so `back_edge_kept` still reports the first. (5) The drift check reports an intent naming a record that does not name it back, but not an ISSUE naming an intent that does not name it back, since that is a loose relation; a reading item carries no loose relation, so from its end a one-way stamp is drift. A shipped intent naming an issue in `wontfix/` is drift, like one in `open/`, because the itd-4 scope asks whether the issue "actually moved to resolved/". The check is not wired into preflight or CI; `--strict` is there for whoever wires it. (6) The historical passages in shipped intents, closed specs and adr-0057 carry a `(historical)` marker beside the retired names, the precedent `development/activity` set, so the new `retired-promote-back-links` ban can gate every current record. (7) The impact is `breaking`: itd-4 declares none, and the rename removes `promoted_to` from `capture list --json`, `promoted_from` from the intent JSON and both link keys from the record page, and makes an unmigrated ledger unreadable until the migration runs.
- iss-2609011026401952The implement loop has no step that ships an intent once its work merges: abcd intent plan moves drafts to planned and abcd spec close moves planned to shipped, but nothing in the build-review-merge routine invokes the second, so an intent whose code is on main sits in planned indefinitely and launch ship composes the changelog only from terminal folders. Two intents delivering a BREAKING CLI change were about to be released with no changelog line, and the omission is invisible because the cut exits 0 - a planned intent is not a refusal, it is simply not seen
- iss-2609181121522692No lint rule checks that an intent's bucket agrees with its specs' buckets, so a mis-merge that files a new planned intent's spec into closed/ (or the intent into shipped/ with its spec still open) lands silently. Observed in the Gropius managed repo on 2026-09-18 at v0.9.0: after a pull request had moved many records planned/ to shipped/ and open/ to closed/, a git merge of a branch adding one NEW planned intent and its open spec ran rename detection against those moves and offered to place the newcomers in shipped/ and closed/. The record gates read each record on its own: spec_lifecycle refuses a status key, intent_lifecycle checks ids and frontmatter placement, and neither reads the linked record's folder, so the pair "intent planned, its spec closed" passes every gate. Relayed from session gropiusllm-56, which called it a process observation rather than an abcd defect; recorded because the detector is cheap and the shape recurs wherever bucket moves and new records meet in one merge. Wanted: a record-lint rule that refuses a planned intent any of whose specs is in closed/ when no open spec remains, and a shipped intent any of whose specs is in open/. This is the inverse of itd-2609111003026787 (the gate for an intent whose work is live while its spec stays open), which reads the tree rather than the buckets, and a sibling of iss-2608290808193471; the bucket-agreement half is mechanical and needs no reading of the code.
- iss-2609290448510918The predecessor-store qualifier sweep reaches past the six intents iss-2609261536147903 named: outside drafts/ and those six, 192 citation sites across about forty intents name a spc-N at or below spc-70 that is not the citing intent's own spec, with no '(predecessor store)' on the line. Some are live cross-references (the cold-reading family's intents cite each other's live specs), and some are the predecessor store's (itd-4, itd-6, itd-29, itd-47 and itd-49 describe pre-rebuild work in its terms), so each site needs the same reading against the live spec it collides with; the specs charter's Two spc-N Namespaces section is the rule. Found while sweeping the pattern of iss-2609261536147903 in lane drainDrift3; a census script over intents/{planned,shipped,disciplines} reproduces the count.
- iss-2610021502084298No check meets the retired role word in an intent's press release at the record: the docs-lint rule roles/retired-role-word roots at the docs, the command pages and the rules, so a shipped or planned intent's press release carrying it is refused only when the site composer renders it as the landing page's hero quote, which is how itd-60's first MET audit broke the site-render gate (iss-2610021446271464). After the R5 reword of itd-60, thirteen shipped press releases still carry the word (itd-65, itd-66, itd-67, itd-69, itd-73, itd-74, itd-93, itd-102, itd-111, itd-114, itd-125, itd-2609111003026787, itd-2609212137129937), so the next MET audit ingested for any of them refuses the render again on a branch that only recorded an audit; the product thinker's ruling R5 (2026-10-02) accepted that a future slip breaks the build and did not ask for this check. Related to iss-2610020731591808, which names the brief, the principles and the personas registry as swept but unguarded.
- itd-7Lifeboats Carry RepoPrompt Workspace Definitions Forward
- itd-8Lifeboats Carry Source Code, Not Just Lessons
- itd-34Intents Promote In Three Flavours — Standalone, Bundle-Member, Discipline
- itd-121Type The Id, Get Your Next Move
- spc-7abcd-intent-quoted-text-create-symmetric
- spc-26type-the-id-get-your-next-move-abcd-id-dispatches-on-a-recor