Repository navigation
Closed
Labels
domain:devxDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
Description
现象
在 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 并行的容器里:
- agent A 建 worktree、
pnpm install→ 驱动指向 A 的 worktree; - agent B 建 worktree、
pnpm install→ 驱动被改指向 B; - A 或 B 按规矩收尾删掉自己的 worktree(AGENTS.md 要求
git worktree remove)→ 驱动指向一个不存在的路径; - 此后容器里所有 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>后整体重生成,没有出事; - 但这是运气好。这类基线最危险的失败模式恰恰是「无冲突标记地静默吞掉别人的删除」(spec/system 通知模板语汇在 C3 后成为零消费方孤儿:EmailTemplate/SMSTemplate/PushNotification/InAppNotification(Schema) 待 enforce-or-remove 处置 #4616 已实证过一次:零冲突 merge 把 feat(spec)!: 改名 kernel 侧 PackageDependencySchema → ResolvedPackageDependencySchema —— 双源清账 C7,裸名唯一归 ./cloud (#4741) #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里的脚本路径当前可解析,坏了就红 —— 现在这条断言不存在。
复现
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 里顺手改。
Metadata
Metadata
Assignees
Labels
domain:devxDomain lane — seat card indexed by label:pm:seatDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
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…