Filed bare for triage by the domain:spec execution seat (session_01G4138K1EG7kQ81FNba5Kp4) after the #14478 stack landed on main at e9fcd6bbd (2026-09-06T03:17Z). Observation, not a graded defect — no fix proposed, no domain:* set, no pm:queue.
The stack was six cards deep (#15676 → #15682, PRs #15814 · #15837 · #15906 · #15938 · #15988 · #16022), each branched off the one below it, landing on a shared trunk branch and then to main as PR #15626. It worked, but three separate pieces of tooling turned out to have no representation of "this PR is stacked on another PR", and each cost real work. All three are measurements from that campaign, not predictions.
1. check:adr-0087-registration — already-registered is base-relative, and a stacked card has two bases
The gate's own header states the rule:
already-registered — every named id must resolve at HEAD and already exist at the merge base. If the id is new in this diff the honest disposition is registered, and the two cannot be confused because base decides.
Card 6/6 renamed a key already covered by a conversion card 5/6 registers. So:
| base |
is turso-config-timeout-to-timeout-ms there? |
honest disposition |
| card 6/6's own base (card 5/6's head) |
yes |
already-registered — exit 0 |
origin/main |
no |
registered — anything else fires R5, severity 3 |
Neither card is wrong. The two base levels demand contradictory dispositions, and R5 consults only the ids this diff adds, with no cross-reference to a sibling changeset carrying registered in the same eventual merge. Writing registered on card 6/6 would have fired R4 against its own base instead.
The workaround that shipped: land all six cards on the trunk, then edit the disposition to registered on the trunk before it merged to main. Verified there — exit 0, .changeset/driver-turso-config-timeout-ms.md [BREAKING+bang] registered turso-config-timeout-to-timeout-ms (new here: …).
Two traps found while pre-verifying that edit, worth recording whether or not this is ever fixed:
- The gate reads the changeset from the committed diff, not the working tree. An uncommitted edit returns the identical finding, so it reads as "the fix does not work".
- Trailing prose on the marker line is parsed as further ids. Keeping the explanatory sentence inline produced "29 of those id(s) do not exist in the ADR-0087 registries at HEAD: —, that, protocol-18, conversion, added, by, …". The
not-required (…) form tolerates prose only because the parentheses delimit the ids.
2. Squash-only × stacked ⇒ one repair lap per descendant, cumulative
Merge commits are refused at the API level:
PUT /repos/objectstack-ai/objectstack/pulls/15906/merge (merge_method: merge)
→ 405 Merge commits are not allowed on this repository
and main carries 0 merge commits in its last 154 (control: the same --merges query does find one on a feature branch, so the zero is a reading).
A squash keeps the content and destroys the ancestry link, so every descendant's merge base rewinds past everything already landed. Measured after card 2/6 landed:
git merge-base --is-ancestor dcd1be0db 43cc4659a → exit 1 (2/6's head is NOT in the trunk's history)
git merge-base 43cc4659a adad660a1 → 140e0b266 (before cards 1/6 AND 2/6)
The descendant then re-applies content the trunk already holds and collides. Three repair laps were paid — cards 3/6, 4/6 and 6/6 — each a merge, a hand resolution and a regeneration pass. The conflicts were benign in shape but not free: card 4/6's four conflicts were all one prose correction made after it forked, and its side was a purely inherited stale snapshot (blobs byte-identical to its fork point).
⭐ The generalisation, which is what makes this worth a card: the cost is not per-stack, it is per-descendant, and it recurs on every landing. A six-deep stack pays it five times if nothing changes.
3. Heavy CI never runs on a stacked PR at all
lint.yml and ci.yml both declare:
on:
pull_request:
branches:
- main
A PR based on a feature branch triggers neither, so none of the six required contexts reports on any stacked head. Measured across this whole stack: every card PR showed 8–11 light checks (Auto Label, Check Changeset, PR Size, docs drift, …) and zero of Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Dogfood Regression Gate, Temporal Conformance.
Consequence: a stack's first real signal arrives only at the trunk PR, after every card is written, reviewed and merged. Six cards' worth of work was verified by local runs quoted into PR bodies — which is a discipline that worked here, but it is a discipline, not a gate. On the trunk PR the real run came back 35/35 green on the first try; that is a fact about this stack's care, not about the arrangement's safety.
Prior art in this repo
#12933 is the same family through a different mechanism: GitHub's speculative queue stacking made merge-queue-triage count one deterministic failure as N distinct victim PRs, because the tooling had no notion that the victims were one stack. That card's phrasing applies here too — the count (or the disposition, or the trigger) is systematically wrong in a knowable, mechanically detectable way, and every reader re-derives it by hand.
For triage
Deliberately not split into three cards: the three costs were measured on one campaign and a reader deciding whether this repo should support stacked series needs them together. Splitting is triage's call. Each has a different owner and a different kind of fix — a gate rule, a repository setting or policy, and a workflow trigger — and it is entirely reasonable to grade them differently, including grading some or all of them "won't fix, stacks are rare enough".
⚠️ What this card does not claim: that stacking was the wrong choice for #14478. Six cards over one contract surface, each independently reviewable, is what the maintainer ruled and it produced a clean landing. The finding is only that the tooling around it is silent about the arrangement, and the silence is paid for by hand each time.
Filed bare for triage by the
domain:specexecution seat (session_01G4138K1EG7kQ81FNba5Kp4) after the #14478 stack landed onmainate9fcd6bbd(2026-09-06T03:17Z). Observation, not a graded defect — no fix proposed, nodomain:*set, nopm:queue.The stack was six cards deep (#15676 → #15682, PRs #15814 · #15837 · #15906 · #15938 · #15988 · #16022), each branched off the one below it, landing on a shared trunk branch and then to
mainas PR #15626. It worked, but three separate pieces of tooling turned out to have no representation of "this PR is stacked on another PR", and each cost real work. All three are measurements from that campaign, not predictions.1.
check:adr-0087-registration—already-registeredis base-relative, and a stacked card has two basesThe gate's own header states the rule:
Card 6/6 renamed a key already covered by a conversion card 5/6 registers. So:
turso-config-timeout-to-timeout-msthere?already-registered— exit 0origin/mainregistered— anything else fires R5, severity 3Neither card is wrong. The two base levels demand contradictory dispositions, and R5 consults only the ids this diff adds, with no cross-reference to a sibling changeset carrying
registeredin the same eventual merge. Writingregisteredon card 6/6 would have fired R4 against its own base instead.The workaround that shipped: land all six cards on the trunk, then edit the disposition to
registeredon the trunk before it merged tomain. Verified there — exit 0,.changeset/driver-turso-config-timeout-ms.md [BREAKING+bang] registered turso-config-timeout-to-timeout-ms (new here: …).Two traps found while pre-verifying that edit, worth recording whether or not this is ever fixed:
not-required (…)form tolerates prose only because the parentheses delimit the ids.2. Squash-only × stacked ⇒ one repair lap per descendant, cumulative
Merge commits are refused at the API level:
and
maincarries 0 merge commits in its last 154 (control: the same--mergesquery does find one on a feature branch, so the zero is a reading).A squash keeps the content and destroys the ancestry link, so every descendant's merge base rewinds past everything already landed. Measured after card 2/6 landed:
The descendant then re-applies content the trunk already holds and collides. Three repair laps were paid — cards 3/6, 4/6 and 6/6 — each a merge, a hand resolution and a regeneration pass. The conflicts were benign in shape but not free: card 4/6's four conflicts were all one prose correction made after it forked, and its side was a purely inherited stale snapshot (blobs byte-identical to its fork point).
⭐ The generalisation, which is what makes this worth a card: the cost is not per-stack, it is per-descendant, and it recurs on every landing. A six-deep stack pays it five times if nothing changes.
3. Heavy CI never runs on a stacked PR at all
lint.ymlandci.ymlboth declare:A PR based on a feature branch triggers neither, so none of the six required contexts reports on any stacked head. Measured across this whole stack: every card PR showed 8–11 light checks (Auto Label, Check Changeset, PR Size, docs drift, …) and zero of
Lint & Repo Gates,TypeScript Type Check,Test Core,Build Core,Dogfood Regression Gate,Temporal Conformance.Consequence: a stack's first real signal arrives only at the trunk PR, after every card is written, reviewed and merged. Six cards' worth of work was verified by local runs quoted into PR bodies — which is a discipline that worked here, but it is a discipline, not a gate. On the trunk PR the real run came back 35/35 green on the first try; that is a fact about this stack's care, not about the arrangement's safety.
Prior art in this repo
#12933 is the same family through a different mechanism: GitHub's speculative queue stacking made
merge-queue-triagecount one deterministic failure as N distinct victim PRs, because the tooling had no notion that the victims were one stack. That card's phrasing applies here too — the count (or the disposition, or the trigger) is systematically wrong in a knowable, mechanically detectable way, and every reader re-derives it by hand.For triage
Deliberately not split into three cards: the three costs were measured on one campaign and a reader deciding whether this repo should support stacked series needs them together. Splitting is triage's call. Each has a different owner and a different kind of fix — a gate rule, a repository setting or policy, and a workflow trigger — and it is entirely reasonable to grade them differently, including grading some or all of them "won't fix, stacks are rare enough".