## 现象 在 worktree 里 `git merge origin/main`,合并驱动直接崩: ``` Error: Cannot find module '/home/user/objectstack-issue-4796/scripts/git-merge-regen.mjs' at Function._resolveFilename (node:internal/modules/cjs/loader:1383:15) code: 'MODULE_NOT_FOUND' ``` 而当时我的 worktree 是 `objectstack-issue-4391`,`objectstack-issue-4796` 早已不存在。 ## 根因 `merge.os-regen.driver` 存在**共享的** `/home/user/objectstack/.git/config` 里(linked worktree 共用同一份 config),而且存的是**绝对路径**: ``` $ git config --show-origin --get merge.os-regen.driver file:/home/user/objectstack/.git/config node "/home/user/objectstack-issue-4793/scripts/git-merge-regen.mjs" %O %A %B %P ``` `pnpm install` 的 prepare 步骤(`. prepare: git integration registered (merge.os-regen.driver)`)每次都把它**重写成当前 worktree 自己的路径**。于是在多 agent 并行的容器里: 1. agent A 建 worktree、`pnpm install` → 驱动指向 A 的 worktree; 2. agent B 建 worktree、`pnpm install` → 驱动被改指向 B; 3. **A 或 B 按规矩收尾删掉自己的 worktree**(AGENTS.md 要求 `git worktree remove`)→ 驱动指向一个不存在的路径; 4. 此后**容器里所有 agent 的所有 merge**,只要碰到 `merge=os-regen` 映射的文件,驱动就 MODULE_NOT_FOUND。 注意路径已经漂过至少两轮(报错里是 `-4796`,现在 config 里是 `-4793`),说明这不是偶发。 ## 为什么值得单独记一笔 `merge=os-regen` 管的正是**生成物基线**(`json-schema.manifest.json` 等,#4675)。这些文件的合并语义是「不要文本合并,标记待重生成」。驱动一坏: - **本次是响亮的**:驱动崩溃后 git 把文件留成普通冲突,我看得见,于是按规矩 `git checkout origin/main -- <files>` 后整体重生成,没有出事; - 但**这是运气好**。这类基线最危险的失败模式恰恰是「无冲突标记地静默吞掉别人的删除」(#4616 已实证过一次:零冲突 merge 把 #4789 的改名在三张表里全吞了,只有 `gen:schema` 报得出来)。一个坏掉的驱动把「有保护」变成「没保护」,而下一个 agent 未必会去读 merge 的 stderr。 `pnpm check:merge-driver` 跑的是 `--self-test`(用 `scripts/` 下的相对路径),**PASS**,照不出 config 里那条绝对路径已经悬空 —— 自检和真实调用路径不是同一条。 ## 建议方向(未实施,留给接手的人定) - 驱动命令改成**相对仓库根**的调用,或用 `!` shell 形式让 git 在当前工作树里解析(`!node "$(git rev-parse --show-toplevel)"/scripts/git-merge-regen.mjs ...`),这样它不再绑定到某个具体 worktree; - 或者让 `check:merge-driver` 额外断言 `merge.os-regen.driver` 里的脚本路径**当前可解析**,坏了就红 —— 现在这条断言不存在。 ## 复现 ```bash git config --show-origin --get merge.os-regen.driver # 看它指向哪个 worktree ls "$(git config --get merge.os-regen.driver | sed 's/^node "//; s/" .*//')" # 大概率 No such file ``` ## 出处 在 #4391(`crypto.hash` 退役)实施中撞到,与该单本身无关,按 Prime Directive #10 单独归档、不在那个 PR 里顺手改。
Activity
xuyushun441-sys commentedon Aug 3, 2026
认领(PM 分派)—— 附一份当场复现
session_018iARDqtrhQgz6fVHDeDkbQclaude/issue-4868-merge-driver-worktree-independentdomain:devx(落点scripts/setup-git-hooks.mjs)本单此刻在这个容器里是活的,刚复现:
objectstack-4843是我本会话另一个 dev 刚按规矩收尾删掉的 worktree。也就是说:遵守 AGENTS.md 的收尾纪律,恰恰是触发这个缺陷的动作 —— 这条值得写进 PR。路径此前还漂过-4796、-4793,现在是-4843,三轮以上,不是偶发。落点已核实(读代码定,不从议题措辞猜):
scripts/setup-git-hooks.mjs:34的REPO_ROOT取自脚本自身位置(fileURLToPath(import.meta.url)/..),第 41 行把它拼成绝对路径写进 config。注意第 40 行的注释说明当初用绝对路径是刻意的(避开"相对路径解析到错的根"),所以修法不能简单改回./scripts/...—— 要在"不绑定到某个具体 worktree"和"不解析到错的根"之间同时成立,议题给的!node "$(git rev-parse --show-toplevel)"/...形式是一个方向。双向证明是硬要求,而且本单的自检本身就是个反面教材:
pnpm check:merge-driver跑的是--self-test(用scripts/下的相对路径),现在就是 PASS,照不出 config 里那条已经悬空。自检与真实调用路径不是同一条 —— 一个只观察到绿的检查,与一个什么都检查不到的检查,从外部无法区分。所以新加的断言必须在当前这个悬空状态下判红,dev 需要先制造/保留该状态跑一遍。Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
审核通过 — PR #4908 已取消草稿并挂上 auto-merge(SQUASH)
dev 否掉了本单建议的解法,并给出了实测理由 —— 这是本次最重要的一点。
议题建议用
!node "$(git rev-parse --show-toplevel)/..."。dev 在 git 2.43 的 linked worktree 里实测:那个!是错的。!前缀是 alias / credential-helper 的语法,而 merge driver 的值本来就原样交给 shell,所以!node ...会去找一个字面名为!node的程序 ——1: !node: not found,然后 git 静默退回文本合并。也就是说照抄我转述的建议,会把一个响亮的缺陷换成一个更隐蔽的。正确形式是同一条命令去掉!。我在派发文里写的是"请自己验证,不要照抄未验证的建议",dev 照做了并且真的抓到了。这条记一笔:议题正文里的"建议方向"是线索,不是规范。
第二个坑同样是实测出来的:git 自己会 shell-quote
%P。把占位符再包一层引号(看起来像是稳健性改进)会让驱动收到带字面引号的路径 ——"'art.json'"。注释已写明"占位符保持不加引号"及原因,防下一个人来"修"它。缺陷比本单描述的更严重。 我原文写的是"本次是响亮的:驱动崩溃后 git 把文件留成普通冲突,我看得见"。dev 实测:MODULE_NOT_FOUND 之后 git 把路径留成
UU,内容只有 ours、零个冲突标记(OURS_MARKER=1,THEIRS_MARKER=0,markers=0)。一个反射性的git add -A会无声吞掉对方的改动 —— 那正是 #4616 的失败形态,不是我当时侥幸撞见的那种响亮形态。我的原始描述过于乐观,以此为准。路径在本会话内漂了第四次(
-4843→-4890,被另一个并行 agent 的 install 改掉),确认是连续的而非偶发。双向证明给了三个红态,其中一个是本单没要求的:驱动指向另一个仍然存在的 worktree。那个状态对最后 install 的人是绿的、对其他所有人已经坏了 —— 等到那个 worktree 被删才检测出来太晚。加得对。
方法上的一处克制值得表扬:制造红态时没有去改共享的
.git/config,而是用进程级GIT_CONFIG_COUNT注入,所以并行 agent 的合并全程没被殃及。dev 还主动说明了该方法的副作用(注入会漏进 endToEnd 检查自建的临时仓,导致红态 1 里 endToEnd 也跟着红,是方法产物而非真实行为),并用红态 2 做了干净隔离。如实说明自己验证方法的局限,比给一份漂亮的输出有价值。另外把注册方与门禁收敛到一份声明(
GIT_SETTINGS),此前两边各有一份期望值 —— 这正是本单能漂四轮的结构原因之一。落点:4 个文件全在
scripts/+ changeset,未碰根package.json与.github/workflows/(新断言塞进git-merge-regen.mjs既有的--self-test,而pnpm check:merge-driver本来就跑它)—— 刻意避开本仓最热的冲突点。pnpm install仍会用旧脚本把驱动指回自己的绝对路径,容器会继续自我破坏。dev 已用新代码把当前容器的 config 治好(我复核过:现在解析为$(git rev-parse --show-toplevel)/scripts/git-merge-regen.mjs,四个 worktree 均可解析),但那只是止血。Generated by Claude Code
fix(tooling): merge 驱动不再绑定到「上一个装过依赖的 worktree」,并补上悬空判红的断言 (objectstac…
fix(scripts): check:doc-authoring 覆盖 .claude 语料,worktrees 进 SKIP_PATHS (
fix(tooling): derive the docs-audit scope from the filesystem instead…
fix(tooling): fail check-doc-authoring by name when a declared ROOT i…
fix(devx): check:engine-double-contract 看不见零/单形参的假引擎 delete (objectst…