Skip to content

fix(grok): 把 #825 的 MCP 就绪 fail-closed 落到 main(rebase + 补两处今天新门抓到的) - #1004

Merged
vansin merged 11 commits into
mainfrom
fix/825-grok-mcp-readiness-landed
Aug 18, 2026
Merged

fix(grok): 把 #825 的 MCP 就绪 fail-closed 落到 main(rebase + 补两处今天新门抓到的)#1004
vansin merged 11 commits into
mainfrom
fix/825-grok-mcp-readiness-landed

Conversation

@vansin

@vansin vansin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

#825 / #813目标是先落地:grok TUI 共存这条线的栈是
#825 → #826 → #830 → {#836, #867},其中只有 #825 的 base 是 main
它不落地,上面四条谁都到不了 main。

为什么另开一条而不是推 #825

#825 是 draft、落后 main 120 个提交、上次 CI 只跑了 10 个 check(今天一个 PR 跑 13–23 个)。
要让它在今天这套门下重跑就必须 push 那条分支 —— 那是别人也在用的共享 draft 分支
force-push 有丢别人工作的风险。

所以本 PR 从我自己的分支发,内容 = 它那 9 个补丁 rebase 到 origin/mainrc=0,0 冲突)

今天新合的两道门抓到了它两处

我上一轮在 #825 上测过「引入新失败 0」,那句话当时是真的 —— 但这两道门是今天晚些
才合进 main 的,门槛抬高之后它就红了。

1. test-suite-registration#1003

tests/test813-grok-mcp-readiness/ 是新增套件,没有任何 CI 会跑它。

🔴 这条特别值得修而不是豁免#825 的目的就是「MCP 没就绪时 fail closed」,
而验证这件事的套件如果不进 CI,fail-closed 这个保证就没有任何东西持续守着它

test831 的同构做法接进 qa.yml:两处 paths 条目 + build/run 两个 step。

⚠️ 一个照抄会踩的坑:它的 Dockerfile 收的 build-arg 叫 SOURCE_COMMIT
(不是 831 的 TEST831_SOURCE_COMMIT),容器里由 ENV TEST813_SOURCE_COMMIT 承接。
照抄 831 的参数名会静默拿不到值

2. doc-symbol-pins#1002

docs/message-lifecycle.mdshouldSkipMessage 钉在 cli.ts#L4639,而 #825
cli.ts 加了 54 行,真值现在是 4662

文本 cli.ts:4639 和 URL #L4639 是同一句话的两副面孔,两处都改 —— 只改 URL 会
留下渲染出来给人读的那个错数字(#1001 刚吃过这个亏)。

验证

rebase 到 origin/main        rc=0,0 冲突,9 个补丁
test-suite-registration      rc=0   suites=198 registered=34 orphans=164 baseline=164 new=0
doc-symbol-pins              rc=0
l1-paths-sync                rc=0
qa-trigger-coverage          rc=0
workflow-structure           rc=0
docs-integrity               rc=0
no-escaped-comments          rc=0

registered 从 33 变 34orphans 仍是 164 —— 新套件是接进 CI 了
不是塞进基线蒙混过关。

合并之后

#825 的 9 个补丁就落在 main 上了,git cherry 会把它判成 patch-equivalent。
届时应当关掉 #825 并在上面说明去向,然后 #826 可以 rebase 到 main 继续往上落。

vansin and others added 10 commits August 18, 2026 22:23
#825(`fix(grok): fail closed when CommHub MCP is not ready`)从 2026-08-15 起是
draft,落后 main 120 个提交,上次 CI 只跑了 10 个 check(今天一个 PR 跑 13–23 个)。
它是 grok 栈 `#825#826#830 → {#836,#867}` 的**唯一出口**,它不落地,
上面四条谁都到不了 main。

**没有 force-push 那条共享 draft 分支** —— 本 PR 从我自己的分支发,内容是它那 9 个
补丁 rebase 到 `origin/main`(`rc=0`,0 冲突)再加下面两处修补。

## 今天新合的两道门抓到了它两处

上一轮我测过「#825 引入新失败 0」,那句话当时是真的 —— 但那两道门是**今天晚些时候**
才合进 main 的,门槛抬高之后它就红了:

  1. `test-suite-registration`(#1003)
     `tests/test813-grok-mcp-readiness/` 是新增套件,没有任何 CI 会跑它。
     🔴 这条特别值得修而不是豁免:**#825 的目的就是「MCP 没就绪时 fail closed」,
     而验证这件事的套件如果不进 CI,fail-closed 这个保证就没有任何东西持续守着。**
     照 test831 的同构做法接进 qa.yml:两处 `paths` + build/run 两个 step。
     ⚠️ 它的 Dockerfile 收的 build-arg 叫 `SOURCE_COMMIT`(不是 831 的
     `TEST831_SOURCE_COMMIT`),容器里由 `ENV TEST813_SOURCE_COMMIT` 承接 ——
     照抄 831 的参数名会静默拿不到值。

  2. `doc-symbol-pins`(#1002)
     `docs/message-lifecycle.md` 把 `shouldSkipMessage` 钉在 `cli.ts#L4639`,
     而 #825 给 cli.ts 加了 54 行,真值现在是 **4662**。
     文本 `cli.ts:4639` 和 URL `#L4639` 是同一句话的两副面孔,两处都改 ——
     只改 URL 会留下渲染出来给人读的那个错数字。

## 验证

    rebase 到 origin/main            rc=0,0 冲突,9 个补丁
    test-suite-registration          rc=0   suites=198 registered=34 orphans=164 new=0
    doc-symbol-pins                  rc=0
    l1-paths-sync / qa-trigger-coverage / workflow-structure / docs-integrity /
    no-escaped-comments              全部 rc=0

`registered` 从 33 变 34、`orphans` 仍是 164 —— 新套件是接进 CI 了,不是塞进基线蒙混。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

把 test813 接进 CI(本 PR 上一个提交)之后,它在 GitHub runner 上第一次运行就红:

    MCP_READINESS_PASS tools=commhub_get_all_status,commhub_send_message,commhub_send_task,commhub_upload_file
    FAIL: mutation survived: upload-tool-removed
    MCP_READINESS_PASS tools=…(同一份列表,commhub_upload_file 仍在)

不是产品没拦住,是**变异根本没发生**:

    sed -i '/^[[:space:]]*"commhub_upload_file",[[:space:]]*$/d' agent-network/src/node-server.ts

那个模式要求整行只有 `"commhub_upload_file",`,而真实那行是
`      name: "commhub_upload_file",`(对象字面量的字段)。实测命中 **0** ——
在 origin/main 上也是 0,**它从写下那天起就没匹配过任何一行**。
套件此前不在任何 CI 里(#861 说的 164 个孤儿之一),所以没人见过它红。

删整行会破坏对象字面量语法,改成改名:

    sed -i 's/name: "commhub_upload_file"/name: "commhub_upload_file_MUT"/' …

干跑验证:文件确实变了(`243:      name: "commhub_upload_file_MUT",`),已还原。

## 顺带堵住这一类,不只这一条

给 `expect_red` 加空变异防护:跑完变异命令后,若两个源文件**都**没被改动就直接判红,
并说清楚是「sed 模式和源码对不上」而不是「产品没拦住」。

这两种结论指向完全不同的下一步 —— **去改产品 vs 去改测试** —— 而它们在没有这道防护
时打印出来是同一句 `mutation survived`。

同一个套件里另外三条 sed 都逐条查过,各自命中 1(其中打 fake-grok.mjs 那条还自带
`grep -Fxc … -eq 1` 基数守卫),只有这一条是空的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vansin
vansin merged commit 89a6164 into main Aug 18, 2026
28 checks passed
@vansin
vansin deleted the fix/825-grok-mcp-readiness-landed branch August 18, 2026 14:43
vansin added a commit that referenced this pull request Aug 18, 2026
* feat(grok): add strict repo-read copresence profile

* docs(tests): record restacked Grok repo-read evidence

* feat(grok): 把 #826 的 strict repo-read 共存档位落到 main

grok 栈 `#825#826#830 → {#836,#867}` 的第二层。#825 已由 #1004 落地
(main `89a6164e`),所以这一层现在可以直接对 main 落。

**没有 force-push #826 那条共享 draft 分支**,本 PR 从我自己的分支发。

## rebase 方式:只取它自己那两个提交

直接 `git rebase origin/main` **会冲突** —— 冲在 `436b4bdb fix: fail closed on broken
Grok CommHub MCP`,那是 **#825 自己的提交**,已经以 squash 形式在 main 上了,而 #826
的历史里还带着它。

正确做法是把它自己那部分接到 main 上:

    git rebase --onto origin/main <#825-head> <#826-head>     rc=0

它自己的两个提交:

    4496835 feat(grok): add strict repo-read copresence profile
    1149676 docs(tests): record restacked Grok repo-read evidence

结果 12 files changed, 284 insertions(+), 36 deletions(-)。

## 今天这套门抓到一处

`doc-symbol-pins`:`docs/message-lifecycle.md` 把 `shouldSkipMessage` 钉在
`cli.ts#L4662`,而本层又往 `cli.ts` 加了行,真值变成 **4669**。文本和 URL 两处都改。

🔴 **这是同一条 pin 今晚第二次漂**(#1004 刚把它从 4639 改到 4662)。
往 `cli.ts` 这种热点文件钉**行号**,几乎每个碰它的 PR 都会把它顶漂 ——
改成只钉符号、不钉行号才是根治,但那是另一条改动,不在这里顺手做。

其余全绿:test-suite-registration(suites=198 registered=34 orphans=164 new=0)/
doc-source-pins / no-escaped-comments / no-memory-slugs / home-path-baseline /
public-script-safety / l1-paths-sync / copresence-profile-pin。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
vansin pushed a commit that referenced this pull request Aug 18, 2026
CI 上 `doc source-pin floor (Docker)` 这个 job 红了(它同时跑 test813):

    MUTATION_RED upload-tool-removed          ← 我在 #1004 修好的那条空变异,现在真的会红了
    MUTATION_RED stale-three-tool-doctor
    PRODUCT_PATH_NEGATIVE_PASS registration=absent session=unchanged
    FAIL: canonical-Bun product path did not reach TUI readiness
    GrokCopresenceFailure: grok copresence pre-spawn audit failed:
      grok copresence refuses any MCP server except its runtime-owned commhub stdio server
      failureCode: 'spawn_audit'

## 为什么

`7914755a fix(grok): bind MCP audit to resolved Bun path` 把审计从

    commhubRecord.target !== "bun"

改成把**解析后的绝对 bun 路径**传进去、两侧 `resolve()` 后再比。而生成那份 MCP 配置的
`resolveGrokCommhubMcpCommand()` 用的是 **`realpathSync()`(解析符号链接)**,
审计侧用的是 **`resolve()`(只归一化路径,不解析符号链接)**。容器里 `bun` 是符号链接时,
两侧就对不上 —— 于是 pre-spawn 审计把自己生成的那份配置也拒了。

## 处理:只摘这一个提交,其余照落

    git rebase --onto <该提交>^ <该提交> <分支>

剩下 7 个提交,`runtime.ts` 那处比较回到 main 现在的写法(`target !== "bun"`)。
**#883 那条 P0 点名的 `b830403b` 和 #881 的修复 `a9df8dba` 都在保留的这 7 个里**,
本次落地的核心价值不受影响。

## 不在这里顺手修 `7914755a` 的理由

它要解决的问题是真的(裸 `"bun"` 比较挡不住指向别处的 bun)。但把它改对需要决定
**审计与生成两侧统一用哪一种解析**(`realpath` vs `resolve`),那是安全审计语义,
要有自己的见红证据;而且它已经被 #836 的核对牵扯进来(`fc221fc5` 是它的前身)。
**留作单独一条,不塞进这条含 P0 的 PR。**

## 顺带

摘掉这 6 行之后 `shouldSkipMessage` 的行号 pin 从 4695 变回 **4691**,已改(文本+URL)。
八道门在本地全 rc=0。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vansin added a commit that referenced this pull request Aug 18, 2026
* fix(grok): preserve safe TUI composer navigation

* test(grok): record safe navigation evidence

* fix(grok): recover after fatal copresence boundary

* docs(test): record communication dog recovery evidence

* fix(grok): correlate batched automatic permissions

* docs(test): record batched approval recovery

* fix(grok): 落地前修掉 #867 的三处 —— 其中一处是能力边界回归

预跑今天这套门时,#867 这一层红了三条。三条都是真的,逐条修,没有走豁免。

## 🔴 1. 能力边界回归(copresence-profile-pin)

    ::error file=agent-node/src/cli.ts,line=76::static import of grok-copresence/policy|runtime.
    ESM static imports are hoisted, so its top level runs BEFORE the profile pin at line 570

#867 加了一条**静态** import:

    import { retireStoppedGrokCopresenceRuntime } from "./runtime/grok-copresence/runtime-retirement";

ESM 静态 import 是提升的 —— 被导入模块的顶层会在 cli.ts 第一条语句之前执行,于是
`policy.ts` 顶层那句 `readPinnedGrokCopresenceCapabilityProfile()` 读到的是 **ambient
环境变量**,而不是启动时从节点配置钉下来的档位。**能力档位就此可以被环境变量绕过。**

改成动态 import,放在调用点、启动钉档位之后,与本文件里另外两处 grok-copresence
的取法一致:`static_imports 1 → 0,dynamic_imports 2 → 3`。

这道门(#937 建的)第一次抓到真东西,而它抓到的是一条**在 draft 里躺了三天**的回归。

## 2. 公开仓里的真人 home 路径(home-path-baseline)

新文件 `docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt` 有 3 处
`/home/<真人名>/`。承重的是 `.commhub/runtime-commdog-…` 那部分,前缀不是,
所以按门给的第一条修法换成 `~/`,证据一字不损,**不抬基线**。

## 3. 行号 pin 第三次漂(doc-symbol-pins)

`docs/message-lifecycle.md` 的 `shouldSkipMessage`:本层又动了 `cli.ts`(含上面第 1 条
的改动),`4669 → 4695`。文本和 URL 两处都改。

(这是它今晚第三次漂:4639 → 4662 → 4669 → 4695。我曾提议「去掉行号根治」,
**那句已撤回** —— 实测去掉 `#L` 会让这条 pin 整个掉出那道门的正则:
`扫到 15→14 / 判定 5→4 / rc 1→0`,是删覆盖不是修问题。)

## 复跑

    home-path-baseline / copresence-profile-pin / doc-symbol-pins / test-suite-registration /
    doc-source-pins / no-escaped-comments / no-memory-slugs / l1-paths-sync    全部 rc=0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(grok): 摘掉 `7914755a` —— 它让 test813 的 canonical-Bun 产品路径起不来

CI 上 `doc source-pin floor (Docker)` 这个 job 红了(它同时跑 test813):

    MUTATION_RED upload-tool-removed          ← 我在 #1004 修好的那条空变异,现在真的会红了
    MUTATION_RED stale-three-tool-doctor
    PRODUCT_PATH_NEGATIVE_PASS registration=absent session=unchanged
    FAIL: canonical-Bun product path did not reach TUI readiness
    GrokCopresenceFailure: grok copresence pre-spawn audit failed:
      grok copresence refuses any MCP server except its runtime-owned commhub stdio server
      failureCode: 'spawn_audit'

## 为什么

`7914755a fix(grok): bind MCP audit to resolved Bun path` 把审计从

    commhubRecord.target !== "bun"

改成把**解析后的绝对 bun 路径**传进去、两侧 `resolve()` 后再比。而生成那份 MCP 配置的
`resolveGrokCommhubMcpCommand()` 用的是 **`realpathSync()`(解析符号链接)**,
审计侧用的是 **`resolve()`(只归一化路径,不解析符号链接)**。容器里 `bun` 是符号链接时,
两侧就对不上 —— 于是 pre-spawn 审计把自己生成的那份配置也拒了。

## 处理:只摘这一个提交,其余照落

    git rebase --onto <该提交>^ <该提交> <分支>

剩下 7 个提交,`runtime.ts` 那处比较回到 main 现在的写法(`target !== "bun"`)。
**#883 那条 P0 点名的 `b830403b` 和 #881 的修复 `a9df8dba` 都在保留的这 7 个里**,
本次落地的核心价值不受影响。

## 不在这里顺手修 `7914755a` 的理由

它要解决的问题是真的(裸 `"bun"` 比较挡不住指向别处的 bun)。但把它改对需要决定
**审计与生成两侧统一用哪一种解析**(`realpath` vs `resolve`),那是安全审计语义,
要有自己的见红证据;而且它已经被 #836 的核对牵扯进来(`fc221fc5` 是它的前身)。
**留作单独一条,不塞进这条含 P0 的 PR。**

## 顺带

摘掉这 6 行之后 `shouldSkipMessage` 的行号 pin 从 4695 变回 **4691**,已改(文本+URL)。
八道门在本地全 rc=0。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: t <t@t>
Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
vansin added a commit that referenced this pull request Aug 18, 2026
## 我造成了什么

- **#1004**(`89a6164e`,今天)把生成侧从 `'command = "bun"'`(硬编码)换成
  `resolveGrokCommhubMcpCommand()` 的返回值 —— **realpath 之后的绝对路径**;
- **#1010**(`f1208449`,今天)我把 `7914755a fix(grok): bind MCP audit to resolved Bun path`
  **外科式摘掉了**,而那正是同一次改动的**审计侧**。

⇒ main 因此停在一对不匹配的半边:**生成侧写绝对路径,审计侧要求裸 `"bun"`。**

我当时摘它的理由(「它让产品拒绝自己生成的配置」)**方向反了**:真正会拒绝的是**缺了它**。
我读到的那次 CI 红来自**夹具**,不来自产品 —— 见下。

## 真 grok 报什么(实测,不是推断)

容器里装钉死的 `0.2.93`、`--network none` 离线跑(宿主那份是 `1.0.5`,且其符号链接被
63 个在跑节点共用,没有动):

    容器内 grok --version → grok 0.2.93 (f00f96316d)
    config.toml:  command = "/usr/local/bin/fake-bun"
    $ grok inspect --json
    "mcpServers": [{ "name":"commhub", "transport":"stdio",
                     "target":"/usr/local/bin/fake-bun",
                     "source":{"type":"configToml","path":"/tmp/gh/config.toml"} }]

⇒ **`target` 逐字等于 config.toml 的 `command`。** 而生成侧那个解析器只可能返回
realpath 之后的绝对路径或抛错 ⇒ **拿裸 `"bun"` 去比永远不成立。**

(顺带:同一件事三条命令三种形状。`mcp doctor --json` 顶层是 `sources`/`servers`,
它的 `target` 是 **command + args 空格拼串**;`mcp list --json` 的字段叫 `command`。
拿错一条就会得出错的结论。)

## 两件必须一起落,分开落任一半就是我今天犯的错

**① 审计侧**(`agent-node/src/runtime/grok-copresence/runtime.ts`)

    -    || commhubRecord.target !== "bun"
    +    || resolve(String(commhubRecord.target || "")) !== resolve(expectedCommhubCommand)

新增第三个参数 `expectedCommhubCommand`(默认 `"bun"` 仅为兼容尚未传参的调用点),
`agent-node/src/cli.ts` 的生产调用点显式传 `commhubMcpCommand`。

**② 夹具**(`tests/test225-grok-preview-package-live/fake-grok.mjs`)

    -      target: "bun",
    +      target: inspectTarget,     // ← 从它自己声称的 configToml 里读 command

不修 ②,①会再一次「看起来让 test813 红」,而红的仍然是夹具。
**原来那个写死的 `"bun"` 不是「夹具不够真」,是它让一个真实的不一致在 CI 里永远看不见** ——
审计要 `"bun"`、夹具就报 `"bun"`,两者恒相符,而产品写的是绝对路径。#1016 就是这么被藏住的。

## 见红 / 见绿

`agent-node/src/runtime/grok-copresence/runtime.test.ts` 加三条,第三条就是 #1016 那个不一致:

    target=绝对路径, expected=同一个绝对路径      → 不抛
    target=绝对路径, expected=另一个绝对路径      → 抛 "runtime-owned commhub"
    target=裸 "bun", expected=绝对路径            → 抛(#1016 本身)

    bun test src/runtime/grok-copresence/runtime.test.ts     54 pass 0 fail
    把审计退回 main 的写法(target !== "bun")再跑          53 pass 1 fail   ← 见红
    还原                                                     54 pass 0 fail

原有 5 条断言仍传 2 个参数、夹具 target 是 `"bun"` ⇒ 默认值让它们照旧通过,向后兼容。

## 没做的

`bun build` 通过(276 modules),但**它不做类型检查**,所以那只证明能打包。
本机没有可用的 tsc(`npx tsc@5` / `typescript@5.9` 都取不到),**类型检查交给 CI**。
test813 的 Docker 套件也交给 CI 跑 —— 那是这次改动真正的验收点。

顺带:`docs/message-lifecycle.md` 的 `shouldSkipMessage` 行号 4691 → 4695(本次给 cli.ts 加了 3 行)。

Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
vansin added a commit that referenced this pull request Aug 18, 2026
## 事实

    4639 → 4662 → 4669 → 4691 → 4695 → 4713 → 4725 → 4728

八个值,对应 #987 / #1004 / #1006 / #1010 / #1017 / #1007 / #1023 / #1039。
规律:**任何往 `agent-node/src/cli.ts` 第 4600 行以上插代码的 PR 都会让它漂**,
那个 PR 就要多跑一轮 CI。这是每次都付的成本,不是偶发。

## 为什么现在才能这么修

`#1024` 里我提过「改成不可变 SHA」,但**当时实测证明它不成立** ——
`check-doc-symbol-pins.py` 的正则捕获了 ref 却从没用过那个捕获组,
SHA 和 `main` 一视同仁,一律拿工作树解析。单变量 A/B 两边逐字相同。

`#1025` 让脚本认了 ref(SHA pin 用 `git show <ref>:<path>` 取那个版本判)。
**这条路是那之后才通的。** 所以本 PR 是 #1025 的下游,不是 #1024 那个被推翻的提案的重提。

## 改动

    - ✅ v1.4.0;当前在 [cli.ts:4725 `shouldSkipMessage`](.../blob/main/agent-node/src/cli.ts#L4725)
    + ✅ v1.4.0;截至 [`04f37800` 的 cli.ts:4725 `shouldSkipMessage`](.../blob/04f37800/agent-node/src/cli.ts#L4725)

**「当前在」也一起改成「截至 `<sha>`」** —— 这不是措辞洁癖:
钉到某个 commit 之后,「当前」这个词就不再被那个链接支持了。
只改链接不改这句话,等于用一个更稳的 pin 去支撑一句更弱的真话。

## 验证:两个方向都见证过,而且**判定数始终是 5**

    未漂时                                rc=0  扫到 15  判定 5  跳过 10  漂移 0
    往 cli.ts 插 3 行(过去 8 次红的场景)  rc=0  扫到 15  判定 5  跳过 10  漂移 0   ← 不再红
    把 pin 的行号改成 100(错的)          rc=1  扫到 15  判定 5  跳过 10  漂移 1   ← 门仍然咬得动
    还原                                  rc=0

**第三行是本 PR 最该看的**:如果这个改法让门不再对错误 pin 判红,那就是把对象移出了取集 ——
「让门变绿」而不是「把问题修好」。它没有:判定数一次都没掉,错的 pin 照样红。

`docs-site` 取集同时核过:扫到 28 / 判定 8 / 跳过 20 / 漂移 0(#1025 之后它才在取集里)。

## 一个操作上的插曲,记一下

我第一次建 worktree 时目录已存在,`git worktree add` 失败 —— **但后面的 `cd` 成功进了那个陈旧目录**,
于是我在一份完全不相干的树上跑了检查,得到 `cli.ts:836` 和 `rc=2`。
**命令失败了,后续步骤照跑。** 后来加了 `cd ... || exit` 并先打印 HEAD 核对才发现。

Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant