雲端的閘:修好三個穩定重現的接線缺陷,並診斷第四個(inkstone/ISEP#90) #96
Reference in New Issue
Block a user
Delete Branch "fix/cloud-wiring-isep90"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
票:
inkstone/ISEP#90雲端不是「地端少幾支閘」,是同一批閘在雲端的行為不一樣。四格,每一格都穩定重現,
而且每一格都不會自己喊痛——這才是它們活這麼久的原因。診斷全文在
docs/governance/cloud-wiring.md。這個 PR 修了什麼
① 未推警察每次收工都誤攔(票上驗收條件 1)
判準從「有沒有 upstream」改成「遠端有沒有這顆 commit」(
git ls-remote,快取 120 秒)。本機的 remote-tracking ref 只答得準「有」——08-28 實測那份
origin/main落後遠端 4 天——所以答「沒有」時才打網路。三態:有/沒有/問不到,問不到一律不報。
順手補:
$CLAUDE_PROJECT_DIR在雲端是薄殼根,真身在$TOP/InkStoneCo/,舊的掃描清單四個路徑在雲端一個都不存在 ⇒ 真身有東西沒推,這支閘一輩子不會知道。
④ 總管的「我確認過了」出口在雲端是焊死的(總管 08-28 補的那一格)
根因查到了,而且不是權限層:三支閘都用
stat -f %m … || stat -c %Y …算戳記的 mtime。GNU coreutils 的-f是「檔案系統資訊」,它一邊回非零、一邊往 stdout 吐一整段區塊 ⇒ 正確的秒數被接在那堆垃圾後面 ⇒
下一行的
*[!0-9]*檢查必然命中 ⇒ 在 Linux(=每一個雲端 session)上,prod-write-guard/main-and-prod-push-guard/stage-before-prod-guard的戳記永遠不會被接受。它們在雲端等於純擋,而閘不會告訴你門是壞的。
修:
hooks/lib/mtime.sh(先 GNU 再 BSD,每一步都驗是不是純數字)。另加
scripts/gate-ok,把七種形狀各異的逃生口收斂成一個名字、一種形狀——沒有弱化任何一道閘,蓋的是同一個檔、同一種語意(單次、綁 repo、綁 session、
有效期全沒動),換掉的只有「怎麼蓋」。
②③ 變成看得見(驗收條件 2 的 ISEP 那半、條件 3、條件 4)
信標在 session 開頭多報三件:版本落差(匿名讀 ISEP main 的 plugin.json,
D20 判準下屬於「讀」)、舊複本遮蔽正門(
InkStoneCo/scripts/ticket就是那件)、退役機制的殘骸(
.claude/pending-verification/)。殘骸的判準是「這一份 plugin 的 hooks/scripts 有沒有提到它」,不是關鍵字黑名單。
訊息改由
hooks/lib/beacon_report.py序列化——三段報告含引號與換行,用 shell 內插拼 JSON 一個引號就會讓整行信標消失,而那正是「零閘狀態」的長相;
fail-open:python 掛掉、檔案不見、網路不通,一律退回原本那一行乾淨的信標。
測試
新增三支(全離線):
main跑同一份hooks/tests/gate-ok.test.shhooks/tests/unpushed-police.test.shhooks/tests/isep-presence-beacon.test.sh⇒ 測試有鑑別力,不是改到寬鬆:該抓的照樣抓,紅的都是原本就壞的那半邊。
gate-ok這支補的正是既有三支測試(29/16/10 條)從來沒驗過的那一半——它們只驗了「擋得住」,一條都沒驗過「放得開」,這就是 ④ 活這麼久的原因。
既有測試無迴歸:
prod-write-guard29/29、stage-before-prod-guard16/16、main-and-prod-push-guard10/10+13/13、release-tag8/8、ticket-api-bypass24/24、ticket-where-seen17/17、baton-handback10/10、comment-carries-task22/22、reply-identity11/11、dispatch-format33/33、search-is-not-proof31/31、factory-idle33/33、mainline-idle61/61、sdd-guard8/8、版本一致性 ✅。併進去之後要做的事
🔴 要發一個新版本號,否則這些改動到不了任何人手上(
docs/TESTING.md開頭那段:plugin update比的是版本號不是內容)。plugin.json這個 PR 刻意沒動——check-version-consistency.sh要求它跟 tag 同一個 commit 一起改。這個 PR 沒有修、也不該由 ISEP 修的兩格
inkstone/ISEP#67InkStoneCo/scripts/ticket這份舊複本(取 token 只認名叫gitea的 remote)inkstone/InkStoneCo真身ISEP 這一側能做的是讓它們不再是看不見的:兩件現在都會在 session 開頭被信標點名。
更新(2026-08-28,併進 main 兩次之後)
已併進
main到bebbbd2(含#94倒數計時器、#97pr-verdict),mergeable已回True。衝突只有docs/TESTING.md一處,解法是兩邊都留、把我的三格往後編成 A16/A17/A18(
main的 A14/A15 原封不動)。盤點數字:在合併後的樹上重數,沒有相加
hooks/*.shhooks.json註冊scripts/頂層scripts/gate-ok)🔴 不能用
ls scripts/ | wc -l:它在這台機器回 40,因為多一個沒進版控的__pycache__/。那個數字會隨「有沒有人跑過 python」而變 ⇒ 用 git 追蹤的數目才穩定。docs/hooks-inventory.md的標題也順手改(它寫「52 支閘」,而自己第 10 行寫「53 個 .sh 檔」——同一頁自相矛盾,標題是漏改的那半)。
version一個字沒動(0.11.0),維持由總管統一收斂。stat -f那一格:補上最後一層(總管指定要補的)總管量到
exit=0、我量到exit=1。兩個都是真的,而分歧本身就是最後一塊拼圖:GNU 的
-f是--file-system,布林旗標、不接格式字串 ⇒%m被當成另一個檔名運算元 ⇒ 離開碼取決於「cwd 裡有沒有一個叫
%m的檔」:兩種都實測重現過(
stat (GNU coreutils) 9.4),而兩種情況閘的結果一模一樣:MT都不是純數字、戳記都作廢。🔴 所以教訓比原本寫的更尖銳:舊寫法的
||fallback 救不了,不是因為它沒跑,而是因為「跑不跑」根本不由這支腳本決定——它由
「cwd 裡有沒有某個檔名」決定。一個行為取決於 cwd 有沒有某個檔的判斷式,
不管跑不跑都是壞的。 ⇒ 修法不能只是「把順序反過來」,每一步都要驗它是不是純數字。
已寫進
hooks/lib/mtime.sh檔頭與docs/governance/cloud-wiring.md,並在
gate-ok.test.sh補第 ⑰ 條守它(cwd 有%m檔時仍要回純數字):16 → 17 條。合併後全部重跑(
CLAUDE_CODE_CHILD_SESSION有設與清掉各跑一次,結果相同)pr-verdict-guard52/52・countdown-guard20/20・prod-write-guard37/37・gate-ok17/17・unpushed-police10/10・isep-presence-beacon14/14・main-and-prod-push10/10+13/13・stage-before-prod16/16+13/13・sdd-guard8/8・reply-identity11/11・dispatch-format33/33・search-is-not-proof31/31・mainline-idle61/61・factory-idle33/33・ticket-api-bypass24/24・ticket-where-seen17/17・baton-handback10/10・comment-carries-task22/22・github-contact14/14・kbdb-api-wall10/10・release-tag8/8・版本一致性 ✅。兩件要總管收斂的(我刻意沒動)
docs/TESTING.md的 A 編號在main上已經撞了四組:A11/A12/A14/A15各出現兩次——四條線各自 append 的結果,跟本 PR 無關(我只保證 A16/A17/A18 不撞)。
要不要統一重編,是總管的事,我不去動別人的段落。
plugin.json的 description 數字沒有可機械復現的規則:查了近 8 次改動,宣告 27 時
scripts/有 30 個檔、宣告 28 時有 34 個——它一直是手寫的。我這次只把它對齊「git 追蹤的頂層項目數」。若總管另有算法,以總管的為準。
三支閘(prod-write-guard/main-and-prod-push-guard/stage-before-prod-guard) 判斷戳記新不新,都寫成這一行: MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0) macOS(BSD stat)上 `-f %m` 就是 mtime,對的。 **GNU coreutils 的 `-f` 是「顯示檔案系統資訊」**,而且它一邊回非零、 一邊往 stdout 吐一整段區塊 ⇒ `||` 接上的秒數被那段文字污染 ⇒ 下一行的 `case "$NOW$MT" in *[!0-9]*) return 1` 必然命中 ⇒ **在 Linux 上那三支閘的戳記永遠不會被接受。** 後果不是少一個便利功能:那三支閘都是「擋下來、但總管看過就能放行」, 而放行那道門在雲端打不開 ⇒ **它們在雲端等於純擋**, 總管照著閘自己印的指示做,做幾次都打不開,閘也不會告訴他門是壞的 (inkstone/InkStoneCo#99 那次「連續四次蓋不出戳記」就是這個形狀)。 · hooks/lib/mtime.sh:先 `-c %Y`(GNU)再 `-f %m`(BSD),**每一步都驗是不是純數字** ——這個 bug 的成因正是「命令失敗了卻還是印了東西」,只看離開碼會再被騙一次。 四支用到的檔各自帶一份 inline fallback:這道門不能因為少一個檔案就再關上一次。 · scripts/gate-ok:把散在各閘訊息裡的七種逃生口收斂成一個名字、一種形狀 (`gate-ok <閘名> [參數]`)。原因是逃生口原本是「臨時湊出來的 Bash 指令」, 而臨時湊的指令沒有穩定形狀可以事先放行——settings.json 的 allow 只能逐條完全比對, 多一個 `&&`、換一個 session id 就落在規則外。它蓋的戳記與閘原本認的完全同一個檔、 同一種語意(單次、綁 repo、綁 session、有效期都沒動),換掉的只有「怎麼蓋」。 測試 hooks/tests/gate-ok.test.sh:16/16。既有的三支測試(29/16/10)不受影響。 拿 main 跑同一份:②⑤(門打得開)紅、⑥⑦⑧(門沒變寬)全綠 ⇒ 這支補的正是既有測試從來沒驗過的那半邊。總管自己驗這一格量到 exit=0,我量到 exit=1。**兩個都是真的**, 而分歧本身就是最後一塊拼圖: **GNU 的 `-f` 是 `--file-system`,布林旗標、不接格式字串** ⇒ `%m` 不是格式,它被當成**另一個檔名運算元** ⇒ 離開碼取決於「cwd 裡有沒有一個叫 `%m` 的檔」: A. 沒有(一般情況) → exit 1 ⇒ `||` 會跑 ⇒ 正確的秒數接在垃圾後面 B. 剛好有 → exit 0 ⇒ `||` 不會跑 ⇒ 整包連一個數字都沒有 兩種情況閘的結果一樣:MT 都不是純數字、戳記都作廢。(實測 GNU coreutils 9.4,兩種都重現過) 🔴 教訓比原本寫的更尖銳:舊寫法的 `||` fallback 救不了, **不是因為它沒跑,而是因為「跑不跑」根本不由這支腳本決定**—— 它由「cwd 裡有沒有某個檔名」決定。 一個行為取決於 cwd 有沒有某個檔的判斷式,不管跑不跑都是壞的。 ⇒ 所以修法不能只是「把順序反過來」,**每一步都要驗它是不是純數字** (lib/mtime.sh 本來就是這樣寫的,現在把理由寫進去了)。 gate-ok 測試補第 ⑰ 條守這一層:cwd 裡有一個叫 `%m` 的檔時,file_mtime 仍要回純數字。 16 → 17 條,TESTING.md 的 A16 一併更新。