雲端的閘:修好三個穩定重現的接線缺陷,並診斷第四個(inkstone/ISEP#90) #96

Merged
claude-code merged 8 commits from fix/cloud-wiring-isep90 into main 2026-08-28 00:37:31 +00:00
Member

票: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-guardmain-and-prod-push-guardstage-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.sh 16/16 ②⑤(門打得開),⑥⑦⑧(門沒變寬)全綠
hooks/tests/unpushed-police.test.sh 10/10 A 群 5 條全紅,B 群 4 條全綠
hooks/tests/isep-presence-beacon.test.sh 14/14

測試有鑑別力,不是改到寬鬆:該抓的照樣抓,紅的都是原本就壞的那半邊。
gate-ok 這支補的正是既有三支測試(29/16/10 條)從來沒驗過的那一半——
它們只驗了「擋得住」,一條都沒驗過「放得開」,這就是 ④ 活這麼久的原因。

既有測試無迴歸:prod-write-guard 29/29、stage-before-prod-guard 16/16、
main-and-prod-push-guard 10/10+13/13、release-tag 8/8、ticket-api-bypass 24/24、
ticket-where-seen 17/17、baton-handback 10/10、comment-carries-task 22/22、
reply-identity 11/11、dispatch-format 33/33、search-is-not-proof 31/31、
factory-idle 33/33、mainline-idle 61/61、sdd-guard 8/8、版本一致性

併進去之後要做的事

🔴 要發一個新版本號,否則這些改動到不了任何人手上(docs/TESTING.md 開頭那段:
plugin update 比的是版本號不是內容)。plugin.json 這個 PR 刻意沒動——
check-version-consistency.sh 要求它跟 tag 同一個 commit 一起改。

這個 PR 沒有修、也不該由 ISEP 修的兩格

缺口 住在哪
雲端載到的版本追上 ISEP main(0.3.9 → 0.9.0,差 7 個 release) inkstone/ISEP#67
InkStoneCo/scripts/ticket 這份舊複本(取 token 只認名叫 gitea 的 remote) inkstone/InkStoneCo 真身

ISEP 這一側能做的是讓它們不再是看不見的:兩件現在都會在 session 開頭被信標點名。


更新(2026-08-28,併進 main 兩次之後)

已併進 mainbebbbd2(含 #94 倒數計時器、#97 pr-verdict),
mergeable 已回 True。衝突只有 docs/TESTING.md 一處,解法是兩邊都留
把我的三格往後編成 A16/A17/A18main 的 A14/A15 原封不動)。

盤點數字:在合併後的樹上重數,沒有相加

動作
hooks/*.sh 53 與 main 已宣告一致,不動
hooks.json 註冊 68 同上,不動
scripts/ 頂層 39 plugin.json 原寫 38 ⇒ 改成 39(本分支加了 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 的檔」:

A. 沒有(一般情況)  → exit 1 ⇒ `||` 會跑   ⇒ 正確的秒數接在垃圾後面
B. 剛好有            → exit 0 ⇒ `||` 不會跑 ⇒ 整包連一個數字都沒有

兩種都實測重現過(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-guard 52/52・countdown-guard 20/20・prod-write-guard 37/37・
gate-ok 17/17・unpushed-police 10/10・isep-presence-beacon 14/14・
main-and-prod-push 10/10+13/13・stage-before-prod 16/16+13/13・sdd-guard 8/8・
reply-identity 11/11・dispatch-format 33/33・search-is-not-proof 31/31・
mainline-idle 61/61・factory-idle 33/33・ticket-api-bypass 24/24・
ticket-where-seen 17/17・baton-handback 10/10・comment-carries-task 22/22・
github-contact 14/14・kbdb-api-wall 10/10・release-tag 8/8・版本一致性

兩件要總管收斂的(我刻意沒動)

  1. docs/TESTING.md 的 A 編號在 main 上已經撞了四組A11A12A14A15
    各出現兩次——四條線各自 append 的結果,跟本 PR 無關(我只保證 A16/A17/A18 不撞)。
    要不要統一重編,是總管的事,我不去動別人的段落。
  2. plugin.json 的 description 數字沒有可機械復現的規則:查了近 8 次改動,
    宣告 27 時 scripts/ 有 30 個檔、宣告 28 時有 34 個——它一直是手寫的。
    我這次只把它對齊「git 追蹤的頂層項目數」。若總管另有算法,以總管的為準。
票:`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.sh` | 16/16 | ②⑤(門打得開)**紅**,⑥⑦⑧(門沒變寬)全綠 | | `hooks/tests/unpushed-police.test.sh` | 10/10 | A 群 5 條**全紅**,B 群 4 條全綠 | | `hooks/tests/isep-presence-beacon.test.sh` | 14/14 | — | ⇒ **測試有鑑別力,不是改到寬鬆**:該抓的照樣抓,紅的都是原本就壞的那半邊。 `gate-ok` 這支補的正是**既有三支測試(29/16/10 條)從來沒驗過的那一半**—— 它們只驗了「擋得住」,一條都沒驗過「放得開」,這就是 ④ 活這麼久的原因。 既有測試無迴歸:`prod-write-guard` 29/29、`stage-before-prod-guard` 16/16、 `main-and-prod-push-guard` 10/10+13/13、`release-tag` 8/8、`ticket-api-bypass` 24/24、 `ticket-where-seen` 17/17、`baton-handback` 10/10、`comment-carries-task` 22/22、 `reply-identity` 11/11、`dispatch-format` 33/33、`search-is-not-proof` 31/31、 `factory-idle` 33/33、`mainline-idle` 61/61、`sdd-guard` 8/8、版本一致性 ✅。 ## 併進去之後要做的事 🔴 **要發一個新版本號**,否則這些改動到不了任何人手上(`docs/TESTING.md` 開頭那段: `plugin update` 比的是版本號不是內容)。`plugin.json` 這個 PR **刻意沒動**—— `check-version-consistency.sh` 要求它跟 tag 同一個 commit 一起改。 ## 這個 PR 沒有修、也不該由 ISEP 修的兩格 | 缺口 | 住在哪 | |---|---| | 雲端載到的版本追上 ISEP main(0.3.9 → 0.9.0,差 7 個 release) | `inkstone/ISEP#67` | | `InkStoneCo/scripts/ticket` 這份舊複本(取 token 只認名叫 `gitea` 的 remote) | `inkstone/InkStoneCo` 真身 | ISEP 這一側能做的是讓它們**不再是看不見的**:兩件現在都會在 session 開頭被信標點名。 --- ## 更新(2026-08-28,併進 main 兩次之後) **已併進 `main` 到 `bebbbd2`**(含 `#94` 倒數計時器、`#97` pr-verdict), `mergeable` 已回 `True`。衝突只有 `docs/TESTING.md` 一處,解法是**兩邊都留**、 把我的三格往後編成 **A16/A17/A18**(`main` 的 A14/A15 原封不動)。 ### 盤點數字:在合併後的樹上重數,沒有相加 | | 數 | 動作 | |---|---|---| | `hooks/*.sh` | 53 | 與 main 已宣告一致,不動 | | `hooks.json` 註冊 | 68 | 同上,不動 | | `scripts/` 頂層 | **39** | plugin.json 原寫 38 ⇒ 改成 39(本分支加了 `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` 的檔」: ``` A. 沒有(一般情況) → exit 1 ⇒ `||` 會跑 ⇒ 正確的秒數接在垃圾後面 B. 剛好有 → exit 0 ⇒ `||` 不會跑 ⇒ 整包連一個數字都沒有 ``` 兩種都實測重現過(`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-guard` 52/52・`countdown-guard` 20/20・`prod-write-guard` 37/37・ `gate-ok` 17/17・`unpushed-police` 10/10・`isep-presence-beacon` 14/14・ `main-and-prod-push` 10/10+13/13・`stage-before-prod` 16/16+13/13・`sdd-guard` 8/8・ `reply-identity` 11/11・`dispatch-format` 33/33・`search-is-not-proof` 31/31・ `mainline-idle` 61/61・`factory-idle` 33/33・`ticket-api-bypass` 24/24・ `ticket-where-seen` 17/17・`baton-handback` 10/10・`comment-carries-task` 22/22・ `github-contact` 14/14・`kbdb-api-wall` 10/10・`release-tag` 8/8・版本一致性 ✅。 ### 兩件要總管收斂的(我刻意沒動) 1. **`docs/TESTING.md` 的 A 編號在 `main` 上已經撞了四組**:`A11`/`A12`/`A14`/`A15` 各出現兩次——四條線各自 append 的結果,**跟本 PR 無關**(我只保證 A16/A17/A18 不撞)。 要不要統一重編,是總管的事,我不去動別人的段落。 2. **`plugin.json` 的 description 數字沒有可機械復現的規則**:查了近 8 次改動, 宣告 27 時 `scripts/` 有 30 個檔、宣告 28 時有 34 個——它一直是手寫的。 我這次只把它對齊「git 追蹤的頂層項目數」。若總管另有算法,以總管的為準。
claude-code added 4 commits 2026-08-28 00:17:42 +00:00
雲端 session 開出來的工作分支天生沒有 upstream,內容卻等於遠端 main
(薄殼 HEAD 081c547 = GitHub 遠端 main 081c547,同一顆)
⇒ 每個雲端 session、每次收工都被攔一次,08-27 一個 session 五次全是誤報。

三件:
· 判準改成問遠端(git ls-remote,快取 120 秒)。本機只答得準「有」,
  答「沒有」時才打網路——實測本 session 的 origin/main 落後遠端 4 天。
· 三態:遠端有/遠端沒有/問不到。**問不到一律不報**,不拿離線當罪證。
· 散落分支的基準也一起換成遠端實際那顆——基準過期會把已經在遠端 main 上的
  分支整批報成散落(包含雲端 session 自己那條)。

順手補雲端那格接線:`$CLAUDE_PROJECT_DIR` 在雲端是薄殼根,真身在 `$TOP/InkStoneCo/`。
舊的掃描清單四個路徑一個都不存在 ⇒ 真身有東西沒推,這支閘一輩子不會知道。

測試 hooks/tests/unpushed-police.test.sh:10/10(全離線,本機 bare repo 當遠端)。
拿舊版跑同一份:A 群 5 條全紅、B 群 4 條全綠 ⇒ 測試有鑑別力,不是改到寬鬆。
信標原本只證明「有一份 plugin 載入了」,不證明「載入的是哪一份」——
2026-08-27 雲端載的是 0.3.9、main 是 0.9.0,中間差 7 個 release,而它照樣是綠的。
那個看不見的落差的實際後果:0.3.9 裡還活著兩支 v0.9.0 已整支刪掉的 hook,
於是 .claude/pending-verification/ 被清掉之後又長回來。

三格(都只報告、不擋、拿不到答案就閉嘴):
· 版本落差:匿名讀 ISEP main 的 plugin.json(D20 判準下屬於「讀」),快取 6 小時、
  逾時 6 秒。落後就講清楚差幾版與怎麼修,並指向 inkstone/ISEP#67。
· 舊複本遮蔽正門:plugin 的 scripts/* 在專案樹(含雲端的 $TOP/InkStoneCo/)裡
  有同名但**內容不同**的複本就點名。實例=InkStoneCo/scripts/ticket 的取 token
  邏輯還停在「只認名叫 gitea 的 remote」,而雲端的 Gitea 叫 origin
  ⇒ 一律死在「拿不到 gitea token」⇒ 人被逼去繞過正門直接打 API,
  而那正是 ticket-api-bypass-guard.sh 在防的事。
· 退役機制的殘骸:.claude/ 底下的目錄,若 plugin 的 hooks/scripts 一個字都沒提到
  ⇒ 產生它的東西已經不在這一份裡了。判準是「plugin 現在還認不認得它」,
  **不是關鍵字黑名單**(leo 2026-08-17 已證明那條路 8 次誤攔、0 次正確攔截)。

訊息改由 hooks/lib/beacon_report.py 序列化:三段報告含引號與換行,
用 shell 內插拼 JSON 一個引號就會讓整行信標消失——而那正是「零閘狀態」的長相。
fail-open:python 掛掉、檔案不見、網路不通,一律退回原本那一行乾淨的信標。

測試 hooks/tests/isep-presence-beacon.test.sh:14/14(全離線)。
第 ⑪ 條守的是寫這支時真的犯過的 bug(內層迴圈變數跟外層的 d 撞名,
第二個殘骸之後全部被靜靜跳過)——把那個 bug 放回去,該條立刻轉紅。
三支閘(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 跑同一份:②⑤(門打得開)紅、⑥⑦⑧(門沒變寬)全綠
⇒ 這支補的正是既有測試從來沒驗過的那半邊。
docs/governance/cloud-wiring.md:四個缺陷分成兩種病——
①④ 是「閘的判準寫的是地端才成立的假設」,②③ 是「同一個東西有兩份,雲端跑到舊的」。
每一格都附實測證據與修法,並標明還沒關掉的兩格住在哪張票(ISEP#67/真身 repo)。

TESTING.md 補 A14/A15/A16 三格:怎麼跑、該看到什麼、什麼算失敗。
三支閘被擋下的訊息也一併指向 scripts/gate-ok(原本的手打法保留當等價寫法)。
claude-code added 2 commits 2026-08-28 00:28:45 +00:00
# Conflicts:
#	docs/TESTING.md
總管自己驗這一格量到 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 一併更新。
claude-code added 2 commits 2026-08-28 00:31:08 +00:00
數字全部在合併後的樹上重數,沒有用相加推的:
· hooks/*.sh = 53、hooks.json 註冊 = 68 —— 與 main 已宣告的一致,不用動
· scripts/ 頂層 = **39**(git 追蹤的),plugin.json 原本寫 38 ⇒ 改成 39
  (本分支新增 scripts/gate-ok 一支)

🔴 這裡不能用 `ls scripts/ | wc -l`:它在這台機器回 40,因為多了一個**沒進版控的
`__pycache__/`**。那個數字會隨「有沒有人跑過 python」而變 ⇒ 用 git 追蹤的數目才穩定。

docs/hooks-inventory.md 標題也順手改:它寫「52 支閘」而自己第 10 行寫「53 個 .sh 檔」
——同一頁自相矛盾,標題是漏改的那半。

**version 一個字沒動**(0.11.0),由總管統一收斂。

合併後全部重跑(CLAUDE_CODE_CHILD_SESSION 有設與清掉各跑一次,結果相同):
pr-verdict-guard 52/52、countdown-guard 20/20、prod-write-guard 37/37、
gate-ok 17/17、unpushed-police 10/10、isep-presence-beacon 14/14、
main-and-prod-push 10/10+13/13、stage-before-prod 16/16+13/13、
sdd-guard 8/8、reply-identity 11/11、dispatch-format 33/33、
search-is-not-proof 31/31、mainline-idle 61/61、factory-idle 33/33、
ticket-api-bypass 24/24、ticket-where-seen 17/17、baton-handback 10/10、
comment-carries-task 22/22、github-contact 14/14、kbdb-api-wall 10/10、
release-tag 8/8、版本一致性 
claude-code self-assigned this 2026-08-28 00:31:43 +00:00
claude-code merged commit 9b0afec0b2 into main 2026-08-28 00:37:31 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: inkstone/ISEP#96