Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7af30e97b0 |
@@ -27,7 +27,7 @@
|
|||||||
| `agents/` | 7 位 | **工人名單**(`inkstone/ISEP#86`)——派工時指名派給誰,規約見 `docs/governance/worker-roster.md` |
|
| `agents/` | 7 位 | **工人名單**(`inkstone/ISEP#86`)——派工時指名派給誰,規約見 `docs/governance/worker-roster.md` |
|
||||||
| `commands/` | 7 支 | `/wiki-recall` `/ship-check` `/cp-write` … |
|
| `commands/` | 7 支 | `/wiki-recall` `/ship-check` `/cp-write` … |
|
||||||
| `skills/` | 2 支 | |
|
| `skills/` | 2 支 | |
|
||||||
| `scripts/` | 48 支 | `ticket`/`roster`/`isep-nag`/`wiki-compress`/`github-arm.sh` …(頂層檔案,不含 `lib/` 等子目錄) |
|
| `scripts/` | 49 支 | `ticket`/`roster`/`isep-nag`/`wiki-compress`/`github-arm.sh` …(頂層檔案,不含 `lib/` 等子目錄) |
|
||||||
|
|
||||||
> 🔴 這五個數字**每次都要在自己的樹上實數**,不准沿用上一版、也不准用加減推
|
> 🔴 這五個數字**每次都要在自己的樹上實數**,不准沿用上一版、也不准用加減推
|
||||||
> (`system-dev/wiki/mistakes.md`:那是 leo 的驗收介面,多報就是假綠):
|
> (`system-dev/wiki/mistakes.md`:那是 leo 的驗收介面,多報就是假綠):
|
||||||
|
|||||||
+31
-1
@@ -129,7 +129,7 @@ python3 scripts/pr-verdict merge inkstone/ISEP#71 --dry-run
|
|||||||
```
|
```
|
||||||
bash hooks/tests/countdown-guard.test.sh
|
bash hooks/tests/countdown-guard.test.sh
|
||||||
```
|
```
|
||||||
**該看到**:`通過 20 條,失敗 0 條`。**全程離線**:時鐘用 `ISEP_COUNTDOWN_NOW` 定住、
|
**該看到**:`通過 21 條,失敗 0 條`。**全程離線**:時鐘用 `ISEP_COUNTDOWN_NOW` 定住、
|
||||||
狀態走 `ISEP_COUNTDOWN_STATE_DIR`,不打網路、不碰 `$HOME`。
|
狀態走 `ISEP_COUNTDOWN_STATE_DIR`,不打網路、不碰 `$HOME`。
|
||||||
|
|
||||||
**它在守什麼**(`inkstone/ISEP#63`,leo 2026-08-27):
|
**它在守什麼**(`inkstone/ISEP#63`,leo 2026-08-27):
|
||||||
@@ -515,6 +515,36 @@ D 群同時釘住反面:認不出來(`cd $VAR`、`cd -`、cd 到不是 repo
|
|||||||
- ① 紅 ⇒ 連總管在自己的目錄裡切分支都被擋。那不是嚴格,那是把共用目錄的主人趕出去
|
- ① 紅 ⇒ 連總管在自己的目錄裡切分支都被擋。那不是嚴格,那是把共用目錄的主人趕出去
|
||||||
- ㉞㉟ 紅 ⇒ `prune` 做錯了。㉞是沒清掉說謊的登記;**㉟是把還在的 worktree 也弄掉了,那是災難**
|
- ㉞㉟ 紅 ⇒ `prune` 做錯了。㉞是沒清掉說謊的登記;**㉟是把還在的 worktree 也弄掉了,那是災難**
|
||||||
|
|
||||||
|
### A25 — 照 A24 開出來的 worktree,不會被跨專案閘擋下:21 條
|
||||||
|
```
|
||||||
|
bash scripts/test-guard-cross-project.sh
|
||||||
|
```
|
||||||
|
**該看到**:`通過 21 條,失敗 0 條`。離線,自己開一棵真的樹(頂層 repo + 4 顆子 repo
|
||||||
|
+ 3 份真的 linked worktree),跑完自動刪掉。
|
||||||
|
|
||||||
|
**它在守什麼**(inkstone/ISEP#117,`inkstone/Arcrun#195` 實撞):
|
||||||
|
A24 那支閘印出來的下一步是 `worktree add <PARENT>/<NAME>-wt-<票號>`,
|
||||||
|
而這支閘的白名單原本是**路徑前綴**比對 ⇒ `matrix/arcrun-wt-195/…` 不符合 `matrix/arcrun/*`
|
||||||
|
⇒ **照 A24 的指示做的人被擋,留在共用目錄裡的人反而過得了**。
|
||||||
|
工人當時只能把 worktree 開到 InkStoneCo 樹之外躲開它——**閘把人逼去繞路,那條規矩就等於沒有**。
|
||||||
|
|
||||||
|
修法:白名單比對的是「這個路徑**屬於哪個 repo**」——`git worktree list --porcelain`
|
||||||
|
第一筆永遠是主工作目錄,主目錄與它所有的 worktree 回同一個答案。
|
||||||
|
`-wt-` 這個命名慣例**一個字都沒有出現在判準裡**(不靠名字=改名字也騙不過去)。
|
||||||
|
|
||||||
|
**失敗**:
|
||||||
|
- ★ 那 6 條任一紅:
|
||||||
|
- 放行那 4 條紅 ⇒ 今天這個誤攔會再發生一次
|
||||||
|
- 「不在名單的 repo,它的 worktree」/「名字像 worktree、其實是空目錄」紅
|
||||||
|
⇒ 判準從「git 回答的事實」滑回「名字看起來像」——那是黑名單的反面,一樣不可靠
|
||||||
|
- 「該擋」那 9 條任一紅 ⇒ 為了修誤攔把該擋的放掉了
|
||||||
|
- 「迴歸」那 5 條任一紅 ⇒ 動到了跟本輪無關的既有規則(`.md` 一律放行、頂層自己的檔)
|
||||||
|
|
||||||
|
**這支測試自己的誠實邊界**:它用 `CLAUDE_CODE_CHILD_SESSION=0` 代表「總管」。
|
||||||
|
真機上那個變數**不可靠**——`InkStoneCo/system-dev/wiki/ops-facts.md` 記著實測:
|
||||||
|
Claude Desktop 的總管主 session 也是 `1`。⇒ 這裡驗的是**閘的分支邏輯**,
|
||||||
|
不是「總管在真機上一定會被擋」。那是另一件事,本輪沒有動它。
|
||||||
|
|
||||||
### A5 — 開票前的搜尋是跨 repo 的
|
### A5 — 開票前的搜尋是跨 repo 的
|
||||||
```
|
```
|
||||||
python3 scripts/ticket where 標籤 模組化
|
python3 scripts/ticket where 標籤 模組化
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
|
|
||||||
| 閘名 | 對你意味著什麼 | 動作 |
|
| 閘名 | 對你意味著什麼 | 動作 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `guard-cross-project.sh` | 總管(頂層)想直接改某個子 repo 的程式碼(非 `.md`)就擋下——頂層只做安排交辦,實作要進那個子 repo 自己做。 | 🛑 擋 |
|
| `guard-cross-project.sh` | 總管(頂層)想直接改某個子 repo 的程式碼(非 `.md`)就擋下——頂層只做安排交辦,實作要進那個子 repo 自己做。白名單上的 repo 給 subagent 動,**連同它照 `line-needs-own-worktree.sh` 開出來的 worktree**(`<repo>-wt-<票號>/`)一起放行——認的是 `git worktree list` 第一筆(這條路徑屬於哪個 repo),不是路徑長什麼樣。 | 🛑 擋 |
|
||||||
| `wiki-secret-scan.sh` | 要寫進 `system-dev/wiki/` 的內容裡出現密碼/金鑰/身分證/信用卡等特徵就擋下,防止機敏資料意外留在會被反覆讀取的記憶空間裡。 | 🛑 擋 |
|
| `wiki-secret-scan.sh` | 要寫進 `system-dev/wiki/` 的內容裡出現密碼/金鑰/身分證/信用卡等特徵就擋下,防止機敏資料意外留在會被反覆讀取的記憶空間裡。 | 🛑 擋 |
|
||||||
| `component-guard.sh` | AI 想自己新建一個零件(component)或亂接 service binding 就擋下——逼它先想「現成零件夠不夠用」,真要建要你解鎖。 | 🛑 擋 |
|
| `component-guard.sh` | AI 想自己新建一個零件(component)或亂接 service binding 就擋下——逼它先想「現成零件夠不夠用」,真要建要你解鎖。 | 🛑 擋 |
|
||||||
| `sdd-guard.sh` | AI 想直接動程式碼檔案,但找不到「唯一一份 active 規格文件(SDD)」對應這件事,或同時有一份以上 active 規格就擋下。 | 🛑 擋 |
|
| `sdd-guard.sh` | AI 想直接動程式碼檔案,但找不到「唯一一份 active 規格文件(SDD)」對應這件事,或同時有一份以上 active 規格就擋下。 | 🛑 擋 |
|
||||||
|
|||||||
@@ -12,8 +12,18 @@
|
|||||||
# 放行:頂層自己的檔(CLAUDE.md / .claude/ / docs/ / MEMORY.md…)、
|
# 放行:頂層自己的檔(CLAUDE.md / .claude/ / docs/ / MEMORY.md…)、
|
||||||
# 以及子 repo 內的 .md(交棒 HANDOFF / 筆記 / 文件——leo 2026-06-15 拍板放行所有 .md)。
|
# 以及子 repo 內的 .md(交棒 HANDOFF / 筆記 / 文件——leo 2026-06-15 拍板放行所有 .md)。
|
||||||
#
|
#
|
||||||
# 誠實限制(arcrun mindset §7):本 hook 擋的是「路徑語法層」——能擋「寫程式碼進子 repo」,
|
# 白名單放行的判準是「**這個路徑屬於哪個 repo**」(git 回答),不是路徑前綴——
|
||||||
# 擋不了「把實作偽裝成 .md」或「該交棒卻判斷成可直改」這種語意層越界。它是底線,不是萬能。
|
# 所以 `matrix/arcrun` 的 worktree(`matrix/arcrun-wt-<票號>/`)跟主工作目錄一樣放行。
|
||||||
|
# 理由與實撞見下面 `owning_repo_rel()` 上方那段(inkstone/ISEP#117)。
|
||||||
|
#
|
||||||
|
# 誠實限制(arcrun mindset §7):
|
||||||
|
# 1. 本 hook 擋的是「路徑語法層」——能擋「寫程式碼進子 repo」,擋不了「把實作偽裝成 .md」
|
||||||
|
# 或「該交棒卻判斷成可直改」這種語意層越界。它是底線,不是萬能。
|
||||||
|
# 2. 🔴 `CLAUDE_CODE_CHILD_SESSION` **不是可靠的「我是 subagent」標記**——
|
||||||
|
# `InkStoneCo/system-dev/wiki/ops-facts.md` 記著實測:Claude Desktop 的總管主 session
|
||||||
|
# 也是 `1`。⇒ 在那個 surface 上,下面整段白名單對總管自己也會放行。
|
||||||
|
# 這一點本輪**沒有動**(那是另一件事,改它要重新評估誰該被擋),
|
||||||
|
# 但誰讀這支閘都該知道它不是它字面上看起來的那道夾子。
|
||||||
|
|
||||||
input="$(cat)"
|
input="$(cat)"
|
||||||
|
|
||||||
@@ -30,13 +40,31 @@ except Exception:
|
|||||||
# 拿不到路徑就放行(不誤擋;hook 不該因解析失敗卡死正常操作)
|
# 拿不到路徑就放行(不誤擋;hook 不該因解析失敗卡死正常操作)
|
||||||
[ -z "$file_path" ] && exit 0
|
[ -z "$file_path" ] && exit 0
|
||||||
|
|
||||||
# 頂層 repo 根(hook 檔位於 <root>/.claude/hooks/)
|
# 頂層 repo 根 =「現在在操作哪個專案」,那正是 $CLAUDE_PROJECT_DIR 的定義。
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
#
|
||||||
|
# 🔴 舊版拿 `<hook 檔>/../..` 當答案。那在 `InkStoneCo/.claude/hooks/` 底下剛好對,
|
||||||
|
# 但這支閘搬進 plugin 之後路徑變成 `<plugin>/hooks/`,`../..` 指到 plugin cache 的
|
||||||
|
# 上一層 ⇒ 絕對路徑的前綴永遠剝不掉 ⇒ rel 永遠留著 `/Users/…` ⇒ 永遠不符合
|
||||||
|
# `matrix/*` ⇒ **這支閘在 plugin 裡從來沒生效過**。
|
||||||
|
# 2026-09-01 實測(inkstone/ISEP#117),同一份 payload(寫 matrix/arcrun/foo.ts):
|
||||||
|
# plugin 那份 → exit 0(放行)
|
||||||
|
# InkStoneCo 那份 → exit 2(擋)
|
||||||
|
# ⇒ 真正在擋的一直是 InkStoneCo 的副本。這也是 README「路徑規約」寫的那條:
|
||||||
|
# 指專案內的檔案要用 `$CLAUDE_PROJECT_DIR`,不要拿 hook 自己的位置去推。
|
||||||
|
# 後面那個 `../..` 只留作 fallback(沒有這個環境變數時,例如手動 pipe 進來測)。
|
||||||
|
repo_root="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
|
||||||
|
# 同一個目錄的**實體路徑**(symlink 解開)。macOS 的 `/tmp`→`/private/tmp`、
|
||||||
|
# `/var`→`/private/var` 就是這個形狀,而 `git worktree list` 回的一律是實體路徑
|
||||||
|
# ⇒ 兩個版本都要留著比,不然「明明是同一個目錄」會比不出來。
|
||||||
|
repo_root_phys="$(cd "$repo_root" 2>/dev/null && pwd -P)"
|
||||||
|
[ -n "${repo_root_phys:-}" ] || repo_root_phys="$repo_root"
|
||||||
|
|
||||||
# 轉成相對 repo_root 的路徑(file_path 可能是絕對或相對)
|
# 轉成相對 repo_root 的路徑(file_path 可能是絕對或相對)
|
||||||
case "$file_path" in
|
case "$file_path" in
|
||||||
/*) rel="${file_path#"$repo_root"/}" ;; # 絕對路徑 → 去掉 repo_root 前綴
|
/*) rel="${file_path#"$repo_root"/}" # 絕對路徑 → 去掉 repo_root 前綴
|
||||||
*) rel="$file_path" ;; # 相對路徑 → 假定相對 repo_root
|
rel="${rel#"$repo_root_phys"/}" ;; # 實體路徑版也試一次
|
||||||
|
*) rel="$file_path" ;; # 相對路徑 → 假定相對 repo_root
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# 不在子 repo 目錄內 → 放行(頂層自己的檔)
|
# 不在子 repo 目錄內 → 放行(頂層自己的檔)
|
||||||
@@ -48,8 +76,10 @@ esac
|
|||||||
# 自動派工放行(2026-06-28 leo 拍板上線,實測通過):
|
# 自動派工放行(2026-06-28 leo 拍板上線,實測通過):
|
||||||
# 子 repo 的 subagent 可直接寫該 repo code(繞 D9)——因 subagent 心智已是該 repo
|
# 子 repo 的 subagent 可直接寫該 repo code(繞 D9)——因 subagent 心智已是該 repo
|
||||||
# (A/B 實驗證 ≈ claude -p),讓「心智是該 repo 的小弟寫該 repo」符合 context 隔離。
|
# (A/B 實驗證 ≈ claude -p),讓「心智是該 repo 的小弟寫該 repo」符合 context 隔離。
|
||||||
# 雙重夾:① CHILD_SESSION=1(harness 內建 subagent 標籤,總管主 session 無此標籤仍被擋)
|
# 雙重夾:① CHILD_SESSION=1 ② 路徑屬於下面白名單 array 上的 repo。
|
||||||
# ② 路徑在下面白名單 array 內。
|
# ⚠️ ①**沒有它字面上看起來那麼緊**:原註解寫「總管主 session 無此標籤仍被擋」,
|
||||||
|
# 但 `InkStoneCo/system-dev/wiki/ops-facts.md` 記著實測——Claude Desktop 的
|
||||||
|
# 總管主 session 也是 `1`。⇒ 在那個 surface 上這一夾是空的。本輪沒有動它。
|
||||||
# 隔離保證=prompt reset(軟,靠監測)非結構級;要絕對純走 claude -p。心法見 wiki。
|
# 隔離保證=prompt reset(軟,靠監測)非結構級;要絕對純走 claude -p。心法見 wiki。
|
||||||
#
|
#
|
||||||
# 🔧 leo 維護:要放開一個 repo 給 subagent 自動寫 code,就在這 array 加一行路徑前綴。
|
# 🔧 leo 維護:要放開一個 repo 給 subagent 自動寫 code,就在這 array 加一行路徑前綴。
|
||||||
@@ -59,11 +89,58 @@ AUTODISPATCH_ALLOW=(
|
|||||||
"matrix/arcrun" # 共用框架(影響全部人,走 PR 把關)
|
"matrix/arcrun" # 共用框架(影響全部人,走 PR 把關)
|
||||||
"products/arcrun-rag" # 產品組裝 repo(2026-07-13 leo 放行 G8:回覆「放行」)
|
"products/arcrun-rag" # 產品組裝 repo(2026-07-13 leo 放行 G8:回覆「放行」)
|
||||||
)
|
)
|
||||||
if [ "$CLAUDE_CODE_CHILD_SESSION" = "1" ]; then
|
|
||||||
|
# ── 白名單比對的是「哪個 repo」,不是「哪個路徑前綴」(inkstone/ISEP#117)─────────
|
||||||
|
#
|
||||||
|
# 為什麼改:`line-needs-own-worktree.sh` 印出來的下一步是
|
||||||
|
# git -C <TOP> worktree add <PARENT>/<NAME>-wt-<票號> -b <分支>
|
||||||
|
# 照做之後,`matrix/arcrun` 的線會在 `matrix/arcrun-wt-195/` 裡動工——
|
||||||
|
# 而舊版這裡是純前綴比對,`matrix/arcrun-wt-195/…` 不符合 `matrix/arcrun/*` ⇒ **擋**。
|
||||||
|
# ⇒ 兩道閘互相打架:**照規矩開 worktree 的人被擋,留在共用目錄裡的人反而過得了**。
|
||||||
|
# 2026-08-31 `inkstone/Arcrun#195` 實撞(見票上原文),工人只好把 worktree 開到
|
||||||
|
# InkStoneCo 樹之外躲開這一支——閘把人逼去繞路,那條規矩就等於沒有。
|
||||||
|
#
|
||||||
|
# 判準用的是 git 自己算出來的事實,跟 `line-needs-own-worktree.sh` 同一個形狀:
|
||||||
|
# `git worktree list --porcelain` **第一筆永遠是主工作目錄**(不管你從哪一份問)。
|
||||||
|
# ⇒ 那一筆就是這個路徑「屬於哪個 repo」的唯一識別碼。
|
||||||
|
# ⇒ 主工作目錄與它所有的 worktree 都會回同一個答案,`-wt-` 這個命名慣例
|
||||||
|
# 一個字都不用出現在判準裡(不靠名字=改名字也騙不過去)。
|
||||||
|
#
|
||||||
|
# 🔴 這是「拿唯一識別碼往**放行**的方向比對」,不是關鍵字比對:
|
||||||
|
# 名單上沒有的 repo、它的 worktree、以及根本不是 repo 的目錄,全部照舊被擋。
|
||||||
|
owning_repo_rel() {
|
||||||
|
local d="$1" main
|
||||||
|
# 檔案還不存在也要答得出來(Write 新檔)⇒ 往上走到第一個真的存在的目錄
|
||||||
|
while [ -n "$d" ] && [ "$d" != "/" ] && [ ! -d "$d" ]; do d="$(dirname "$d")"; done
|
||||||
|
[ -d "$d" ] || return 1
|
||||||
|
main="$(git -C "$d" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p')"
|
||||||
|
[ -n "$main" ] || return 1
|
||||||
|
# 🔴 `git worktree list` 回的一律是**實體路徑**(symlink 都解開了)。
|
||||||
|
# macOS 的 `/tmp`→`/private/tmp`、`/var`→`/private/var` 就是這個形狀,
|
||||||
|
# 所以兩邊都要用實體路徑比,不然「明明是同一個目錄」會比不出來。
|
||||||
|
case "$main" in
|
||||||
|
# 只認「頂層樹底下的子 repo」。走到這裡若答案是頂層樹自己(=那個目錄根本
|
||||||
|
# 不是獨立的 repo,git 一路往上找到了頂層),就回 1 ⇒ 照舊擋,不放寬。
|
||||||
|
"$repo_root"/*) printf '%s' "${main#"$repo_root"/}" ;;
|
||||||
|
"$repo_root_phys"/*) printf '%s' "${main#"$repo_root_phys"/}" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${CLAUDE_CODE_CHILD_SESSION:-}" = "1" ]; then
|
||||||
|
case "$file_path" in
|
||||||
|
/*) abs="$file_path" ;;
|
||||||
|
*) abs="$repo_root/$file_path" ;;
|
||||||
|
esac
|
||||||
|
owner="$(owning_repo_rel "$(dirname "$abs")" 2>/dev/null || true)"
|
||||||
for allowed in "${AUTODISPATCH_ALLOW[@]}"; do
|
for allowed in "${AUTODISPATCH_ALLOW[@]}"; do
|
||||||
case "$rel" in
|
case "$rel" in
|
||||||
"$allowed"/*) echo "🤝 [guard] subagent 放行:$allowed(自動派工)" >&2; exit 0 ;;
|
"$allowed"/*) echo "🤝 [guard] subagent 放行:$allowed(自動派工)" >&2; exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
if [ -n "${owner:-}" ] && [ "$owner" = "$allowed" ]; then
|
||||||
|
echo "🤝 [guard] subagent 放行:$rel 屬於 $allowed 的 worktree(git worktree list 第一筆=$allowed)" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Executable
+88
@@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 跨專案越界閘的測試(docs/TESTING.md A25)— inkstone/ISEP#117
|
||||||
|
#
|
||||||
|
# 判準:白名單放行的是「**哪個 repo**」,不是「哪個路徑前綴」。
|
||||||
|
# ⇒ `matrix/arcrun` 的 worktree(照 line-needs-own-worktree.sh 印的那行開出來的
|
||||||
|
# `matrix/arcrun-wt-<票號>/`)要跟主工作目錄一樣放行;
|
||||||
|
# ⇒ 名單上沒有的子 repo、它的 worktree、根本不是 repo 的目錄,全部照舊擋。
|
||||||
|
#
|
||||||
|
# 這支測試用**真的 git repo + 真的 worktree**當道具(不是假路徑字串)——
|
||||||
|
# 因為被測的判準就是 `git worktree list` 的第一筆,用假路徑等於沒驗到判準本體。
|
||||||
|
#
|
||||||
|
# ⚠️ 誠實邊界:本測試用 `CLAUDE_CODE_CHILD_SESSION=0` 代表「總管」。
|
||||||
|
# 真實環境裡那個變數不可靠(Claude Desktop 的主 session 也是 1,
|
||||||
|
# 見 InkStoneCo/system-dev/wiki/ops-facts.md)。這裡驗的是**閘的分支邏輯**,
|
||||||
|
# 不是「總管在真機上一定會被擋」——後者是另一件事,本輪沒動。
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
H="$PWD/hooks/guard-cross-project.sh"
|
||||||
|
|
||||||
|
FIX="$(mktemp -d "${TMPDIR:-/tmp}/isep117.XXXXXX")"
|
||||||
|
trap 'rm -rf "$FIX"' EXIT
|
||||||
|
|
||||||
|
g() { git -C "$1" -c user.email=t@t -c user.name=t "${@:2}"; }
|
||||||
|
mkrepo() { # mkrepo <路徑>
|
||||||
|
mkdir -p "$1" && git -C "$1" init -q 2>/dev/null
|
||||||
|
: > "$1/seed"; g "$1" add -A >/dev/null 2>&1; g "$1" commit -qm seed >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT="$FIX/InkStoneCo"
|
||||||
|
mkrepo "$ROOT" # 頂層樹本身也是一個 repo
|
||||||
|
mkrepo "$ROOT/matrix/arcrun" # 白名單上的子 repo
|
||||||
|
mkrepo "$ROOT/products/arcrun-rag" # 白名單上的子 repo
|
||||||
|
mkrepo "$ROOT/matrix/other" # **不在**白名單上的子 repo
|
||||||
|
mkdir -p "$ROOT/matrix/plain-dir" # 根本不是 repo
|
||||||
|
mkdir -p "$ROOT/_archive/old"
|
||||||
|
|
||||||
|
# 照 line-needs-own-worktree.sh 印出來的那一行原樣開(<NAME>-wt-<票號>,兄弟目錄)
|
||||||
|
g "$ROOT/matrix/arcrun" worktree add -q "$ROOT/matrix/arcrun-wt-117" -b wt117 >/dev/null 2>&1
|
||||||
|
g "$ROOT/products/arcrun-rag" worktree add -q "$ROOT/products/arcrun-rag-wt-99" -b wt99 >/dev/null 2>&1
|
||||||
|
g "$ROOT/matrix/other" worktree add -q "$ROOT/matrix/other-wt-1" -b wt1 >/dev/null 2>&1
|
||||||
|
# 名字長得像 worktree、實際上只是個空目錄
|
||||||
|
mkdir -p "$ROOT/matrix/arcrun-wt-fake"
|
||||||
|
|
||||||
|
# macOS 的 $TMPDIR 是 /var/...(symlink),git 回的是 /private/var/... ⇒ 兩種形式都要驗
|
||||||
|
ROOT_PHYS="$(cd "$ROOT" && pwd -P)"
|
||||||
|
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
run() { # run <want> <CHILD_SESSION> <file_path> <說明>
|
||||||
|
local want=$1 child=$2 fp=$3 note=$4 got
|
||||||
|
printf '%s' "{\"tool_input\":{\"file_path\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$fp")}}" \
|
||||||
|
| CLAUDE_PROJECT_DIR="$ROOT" CLAUDE_CODE_CHILD_SESSION="$child" bash "$H" >/dev/null 2>&1
|
||||||
|
got=$?
|
||||||
|
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||||||
|
printf 'want=%s got=%s child=%s %s\n' "$want" "$got" "$child" "$note"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "── 該放行:subagent 在白名單 repo 裡(含照閘 A 開出來的 worktree)──"
|
||||||
|
run 0 1 "matrix/arcrun/src/foo.ts" "主工作目錄(迴歸:本來就該過)"
|
||||||
|
run 0 1 "matrix/arcrun-wt-117/src/foo.ts" "★ 本票的病:照閘 A 開的 worktree"
|
||||||
|
run 0 1 "$ROOT/matrix/arcrun-wt-117/src/foo.ts" "★ 同上,絕對路徑"
|
||||||
|
run 0 1 "matrix/arcrun-wt-117/a/b/c/還沒建的檔.ts" "worktree 裡的新檔(目錄還不存在)"
|
||||||
|
run 0 1 "products/arcrun-rag-wt-99/main.go" "另一個白名單 repo 的 worktree"
|
||||||
|
run 0 1 "products/arcrun-rag/main.go" "主工作目錄(迴歸)"
|
||||||
|
run 0 1 "$ROOT_PHYS/matrix/arcrun-wt-117/src/foo.ts" "★ 絕對路徑是實體形式(/private/var vs /var)"
|
||||||
|
|
||||||
|
echo "── 該放行:跟白名單無關的既有規則(迴歸,不能被這次改動弄壞)──"
|
||||||
|
run 0 0 "matrix/arcrun/docs/HANDOFF-x.md" ".md 一律放行(leo 2026-06-15 拍板)"
|
||||||
|
run 0 0 "matrix/other/README.md" "非白名單 repo 的 .md 也放行"
|
||||||
|
run 0 0 "system-dev/wiki/tool.py" "頂層自己的檔"
|
||||||
|
run 0 0 ".claude/hooks/x.sh" "頂層自己的檔"
|
||||||
|
run 0 1 "/tmp/somewhere-else/x.ts" "頂層樹之外(本來就不管)"
|
||||||
|
|
||||||
|
echo "── 該擋:總管(非 subagent)寫實作進子 repo ──"
|
||||||
|
run 2 0 "matrix/arcrun/src/foo.ts" "白名單只對 subagent 開,總管照擋"
|
||||||
|
run 2 0 "matrix/arcrun-wt-117/src/foo.ts" "★ worktree 不是總管的後門"
|
||||||
|
run 2 0 "products/arcrun-rag/main.go" "同上,另一個 repo"
|
||||||
|
run 2 0 "_archive/old/x.py" "_archive 也在管轄內"
|
||||||
|
|
||||||
|
echo "── 該擋:subagent,但那個 repo 不在白名單上 ──"
|
||||||
|
run 2 1 "matrix/other/src/foo.ts" "不在名單的子 repo"
|
||||||
|
run 2 1 "matrix/other-wt-1/src/foo.ts" "★ 不在名單的 repo,它的 worktree 一樣擋"
|
||||||
|
run 2 1 "matrix/plain-dir/foo.ts" "根本不是 repo(git 往上找到頂層樹)"
|
||||||
|
run 2 1 "matrix/arcrun-wt-fake/foo.ts" "★ 名字像 worktree、其實是空目錄 ⇒ 名字騙不過去"
|
||||||
|
run 2 1 "_archive/old/x.py" "_archive 不在白名單上"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "通過 $PASS 條,失敗 $FAIL 條"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
@@ -118,3 +118,47 @@ leo 當場:「**這些為什麼不寫到票裡?**」
|
|||||||
只有這次成立 ⇒ 寫進那張票。**兩種都不進派工單。**
|
只有這次成立 ⇒ 寫進那張票。**兩種都不進派工單。**
|
||||||
|
|
||||||
日期: 2026-08-27(`inkstone/ISEP#30` comment 4327)
|
日期: 2026-08-27(`inkstone/ISEP#30` comment 4327)
|
||||||
|
|
||||||
|
## ⚠️ MISTAKE: 兩道閘互相打架——照 A 做的人被 B 擋,繞過去的人反而過得了
|
||||||
|
|
||||||
|
票: `inkstone/ISEP#117`(實撞在 `inkstone/Arcrun#195`)
|
||||||
|
日期: 2026-09-01
|
||||||
|
|
||||||
|
症狀: `line-needs-own-worktree.sh` 擋下在共用目錄切分支,並印出下一步
|
||||||
|
`worktree add <PARENT>/<NAME>-wt-<票號>`。工人照做,
|
||||||
|
接著在 `matrix/arcrun-wt-195/` 裡寫檔 ⇒ 被 `guard-cross-project.sh` 擋。
|
||||||
|
它的白名單是**路徑前綴**(`matrix/arcrun/*`),對不上 `matrix/arcrun-wt-195/…`。
|
||||||
|
工人的處置:把 worktree 開到 InkStoneCo 樹之外躲開那一支。
|
||||||
|
|
||||||
|
原因: 兩支閘各自都對,**但一支的「出路」不在另一支的「放行條件」裡**。
|
||||||
|
白名單本來就想放行這件事(`CHILD_SESSION=1` + repo 在名單上),
|
||||||
|
只是判準用的是「路徑長什麼樣」,而 A 那支剛好會改變路徑長什麼樣。
|
||||||
|
|
||||||
|
正確做法: 放行的判準改成「這條路徑**屬於哪個 repo**」——
|
||||||
|
`git worktree list --porcelain` 第一筆永遠是主工作目錄,
|
||||||
|
主目錄與它所有的 worktree 回同一個答案。`-wt-` 這個命名慣例
|
||||||
|
一個字都沒有進判準(不靠名字=改名字也騙不過去)。
|
||||||
|
🔴 **推廣**:一支閘印出來的「出路」,就是別支閘必須放行的東西。
|
||||||
|
新增或修改任何會給下一步的閘時,把那個下一步真的做一遍走到底——
|
||||||
|
**出路走不通的閘,會逼人繞路,而繞路一旦成立,那條規矩就等於沒有。**
|
||||||
|
|
||||||
|
## ⚠️ MISTAKE: plugin 裡那支閘一直是啞的,而沒有任何東西會喊一聲
|
||||||
|
|
||||||
|
票: `inkstone/ISEP#117`
|
||||||
|
日期: 2026-09-01
|
||||||
|
|
||||||
|
症狀: 同一份 payload(寫 `matrix/arcrun/foo.ts`)餵給兩份 `guard-cross-project.sh`:
|
||||||
|
plugin 那份 exit 0(放行)、`InkStoneCo/.claude/hooks/` 那份 exit 2(擋)。
|
||||||
|
兩個檔案 `diff` **一個字都不差**。
|
||||||
|
|
||||||
|
原因: 它用 `<hook 檔>/../..` 當「頂層 repo 根」。那在 `InkStoneCo/.claude/hooks/`
|
||||||
|
底下剛好等於 repo 根,搬進 plugin(`<plugin>/hooks/`)之後就指到 plugin cache
|
||||||
|
的上一層 ⇒ 絕對路徑的前綴永遠剝不掉 ⇒ 判斷「在不在子 repo 裡」永遠是否
|
||||||
|
⇒ **它每一次都放行,而放行是靜默的**。
|
||||||
|
|
||||||
|
正確做法: 指專案內的東西用 `$CLAUDE_PROJECT_DIR`(README 路徑規約本來就這樣寫)。
|
||||||
|
🔴 **推廣**:閘的失效方向幾乎都是「靜默放行」——
|
||||||
|
擋錯了有人會喊,放行不會有人喊。所以搬移/複製一支閘之後,
|
||||||
|
**必須實測一次「它該擋的東西現在還擋不擋」**,
|
||||||
|
不要用 `diff` 一致就當它行為一致(這次兩份就是一個字不差、行為相反)。
|
||||||
|
同款:`mistakes.md` 上面那條「裝好了不等於它在跑」。
|
||||||
|
|||||||
Reference in New Issue
Block a user