一個 repo 只有一個資料夾:分身住 repo 裡面的 .worktrees/,收工由同一支閘收掉(inkstone/ISEP#147)
leo 2026-09-07:「一個 repo 只看得到一個資料夾」「禁止這樣產出一大堆資料夾」。 根因:line-needs-own-worktree.sh 教工人開在 repo 旁邊,「收工 remove」只寫在閘訊息裡、沒機制驗, 每條線留一份 ⇒ tech_projects/ 長出 ISEP-wt-117/InkStoneCo-wt-112…一排。規則在文字裡不在機器上。 - scripts/worktree(新):open/close/list/sweep 四個動詞。分身住 <repo>/.worktrees/<票 repo>-<N> (走 .git/info/exclude,不動被追蹤的檔);「推了沒」只有這一份判準(三態:有/沒有/問不到, 問不到不當成沒推也不收);sweep 預設只列不動,--apply 只收「推了且乾淨」,--skip 是操作者點名。 - hooks/line-needs-own-worktree.sh:擋下時印 `scripts/worktree open`(絕對路徑,貼了就跑); 同一支多掛 PostToolUse Agent|Task——從【工單】認出分身,推了收、沒推點名分支+路徑(exit 2), 不 force、不刪。+0 支、+1 條(61 支/86 條,當場數的)。 - docs/governance/worktree-placement.md(新)+ principles 一行:禁令與閘互指。 - 測試:line-needs-own-worktree.test.sh 77→110(G 群:閘印的 open 那行原樣跑、旁邊不多任何東西; H 群:PostToolUse 推了收/沒推點名/安靜的方向);scripts/test-worktree.sh 42 條(七個分身的 fixture, 名字照 leo 看到的那排)。舊閘跑新測試 90/110,紅的 20 條正是這次改到的行為。 - permissions-allow.json 加 worktree 四種形狀(出路在雲端被分類器擋就走不通)。 - README/plugin.json/hooks-inventory 的數字在這棵樹上實數:61 支、86 條、62 支腳本。 待總管定版(改了會被載入的東西)。沒碰 hooks/lib/mainline.py。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
This commit is contained in:
@@ -32,6 +32,10 @@ mkdir -p "$REPO"
|
||||
git -C "$REPO" init -q -b main
|
||||
git -C "$REPO" config user.email t@t; git -C "$REPO" config user.name t
|
||||
echo hi > "$REPO/a.txt"; git -C "$REPO" add -A; git -C "$REPO" commit -qm init
|
||||
# G/H 群要驗「推了沒」——給共用 repo 一顆真的(本機 bare)遠端。離線,不打網路。
|
||||
git init -q --bare "$ROOT/remote.git"
|
||||
git -C "$REPO" remote add origin "$ROOT/remote.git"
|
||||
git -C "$REPO" push -q origin main 2>/dev/null
|
||||
git -C "$REPO" worktree add -q "$ROOT/own-wt" -b mine
|
||||
OWN="$ROOT/own-wt"
|
||||
PLAIN="$ROOT/not-a-repo"; mkdir -p "$PLAIN"
|
||||
@@ -116,11 +120,13 @@ echo "── C 群:訊息與副作用 ──"
|
||||
|
||||
t 2 "㉔ 擋下來(下面幾條檢查這一次的訊息)" "git checkout -b feat/msg"
|
||||
has "$REPO" "㉕ 說得出是哪個目錄"
|
||||
has "worktree add" "㉖ 給得出開自己 worktree 的指令"
|
||||
has "scripts/worktree open" "㉖ 給得出開自己 worktree 的指令(scripts/worktree open,inkstone/ISEP#147)"
|
||||
has "不要開在 /tmp" "㉗ 講得出不要開在 /tmp(08-29 實測:那些全 prunable)"
|
||||
has "WORKTREE_OK=1" "㉘ 講得出逃生門"
|
||||
has "worktree prune" "㉙ 講得出收工要怎麼收"
|
||||
has "scripts/worktree close" "㉙ 講得出收工要怎麼收(scripts/worktree close)"
|
||||
hasnt '$(' "㉚ 訊息沒有被 shell 展開"
|
||||
hasnt "worktree add $ROOT/shared-repo-wt-" "㉚b 不再教人開在 repo 旁邊(09-07 那排分身就是這行教出來的)"
|
||||
has "$REPO/.worktrees/" "㉚c 教的是 repo 裡面的 .worktrees/"
|
||||
|
||||
# 驗收條件 7:說謊的登記真的被清掉
|
||||
git -C "$REPO" worktree add -q "$ROOT/ghost" -b ghosted
|
||||
@@ -285,6 +291,95 @@ t 2 "(77) 同一句換成分號:cat <<'EOF' … EOF; git checkout main ⇒ 照
|
||||
EOF
|
||||
; git checkout main"
|
||||
|
||||
echo
|
||||
echo "── G 群:分身開在 repo 裡面,旁邊不多任何東西(inkstone/ISEP#147)──"
|
||||
# 為什麼這群非有不可:閘訊息教的「開在 repo 旁邊」一份都沒掉、也一份都沒收,
|
||||
# leo 在 Finder 看到 ISEP-wt-117/ISEP-wt115/InkStoneCo-wt-112 一排。
|
||||
# 這群的測資是**閘自己印出來的那一行**(跟 E 群同一個原則):抽出來、只填票號與分支、真的跑,
|
||||
# 然後看三件事實:分身在 .worktrees/ 底下、repo 旁邊的目錄清單一個都沒多、共用目錄 git status 乾淨。
|
||||
BESIDE_BEFORE=$(ls -1 "$ROOT" | sort)
|
||||
t 2 "(78) 先擋一次,拿它印出來的 open 那一行當測資" "git checkout -b feat/g" "$REPO"
|
||||
OPEN_LINE=$(printf '%s' "$LAST_ERR" | grep -F 'scripts/worktree open' | head -1 | sed 's/^[[:space:]]*//; s|<owner/repo#票號>|inkstone/shared-repo#901|; s|<你的分支>|feat/901|')
|
||||
N=$((N+1))
|
||||
if [ -n "$OPEN_LINE" ]; then printf ' ✅ (79) 抽得出那一行:%s\n' "$OPEN_LINE"; PASS=$((PASS+1))
|
||||
else printf ' ❌ (79) 訊息裡抽不出 scripts/worktree open 那一行\n'; FAIL=$((FAIL+1)); fi
|
||||
t 0 "(80) 那一行原樣跑(線、人站在別處)⇒ 這道閘不擋它(open 不是 checkout)" "$OPEN_LINE" "$PLAIN"
|
||||
N=$((N+1))
|
||||
if (cd "$PLAIN" && eval "$OPEN_LINE" >/dev/null 2>&1) && [ -d "$REPO/.worktrees/shared-repo-901" ]; then
|
||||
printf ' ✅ (80b) 真的跑:分身長在 %s/.worktrees/shared-repo-901\n' "$REPO"; PASS=$((PASS+1))
|
||||
else printf ' ❌ (80b) 閘印的 open 那一行跑不動、或分身不在 .worktrees/ 底下\n'; FAIL=$((FAIL+1)); fi
|
||||
N=$((N+1))
|
||||
if [ "$(ls -1 "$ROOT" | sort)" = "$BESIDE_BEFORE" ]; then
|
||||
printf ' ✅ (81) repo 旁邊的目錄清單一個都沒多(leo 在 Finder 只看得到一個資料夾)\n'; PASS=$((PASS+1))
|
||||
else printf ' ❌ (81) repo 旁邊多了東西:%s\n' "$(comm -13 <(printf '%s\n' "$BESIDE_BEFORE") <(ls -1 "$ROOT" | sort) | tr '\n' ' ')"; FAIL=$((FAIL+1)); fi
|
||||
N=$((N+1))
|
||||
if [ -z "$(git -C "$REPO" status --porcelain 2>/dev/null)" ]; then
|
||||
printf ' ✅ (82) 共用目錄 git status 乾淨(.worktrees/ 進了 .git/info/exclude,沒動被追蹤的檔)\n'; PASS=$((PASS+1))
|
||||
else printf ' ❌ (82) 共用目錄髒了:%s\n' "$(git -C "$REPO" status --porcelain | head -3 | tr '\n' ' ')"; FAIL=$((FAIL+1)); fi
|
||||
t 0 "(83) 在那份分身裡切分支 ⇒ 放行(它是自己的 worktree)" "git checkout -b feat/g2" "$REPO/.worktrees/shared-repo-901"
|
||||
t 0 "(84) cd 進分身再切 ⇒ 放行" "cd $REPO/.worktrees/shared-repo-901 && git checkout feat/901" "$PLAIN"
|
||||
# 同一行再跑一次要冪等(重派同一張票的線不該撞牆)
|
||||
N=$((N+1))
|
||||
if (cd "$PLAIN" && eval "$OPEN_LINE" >/dev/null 2>&1); then printf ' ✅ (85) open 冪等:同一張票再開一次不炸\n'; PASS=$((PASS+1))
|
||||
else printf ' ❌ (85) 同一張票第二次 open 失敗\n'; FAIL=$((FAIL+1)); fi
|
||||
|
||||
echo
|
||||
echo "── H 群:收工那一半(PostToolUse Agent|Task)——推了收掉、沒推點名、不靜默(inkstone/ISEP#147)──"
|
||||
# 這裡跑的是**總管的 session**(CLAUDE_CODE_CHILD_SESSION 沒設),掃的範圍由 CLAUDE_PROJECT_DIR 給。
|
||||
post() { # post <prompt> [tool_name]
|
||||
python3 - "$1" "${2:-Agent}" "$ROOT" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"session_id": "S-WT", "hook_event_name": "PostToolUse", "cwd": sys.argv[3],
|
||||
"tool_name": sys.argv[2], "tool_input": {"prompt": sys.argv[1]}, "tool_response": {}},
|
||||
ensure_ascii=False))
|
||||
PY
|
||||
}
|
||||
tp() { # tp <期望 exit> <說明> <prompt> [tool_name] [env]
|
||||
local want="$1" desc="$2" prompt="$3" tool="${4:-Agent}" envs="${5:-CLAUDE_CODE_CHILD_SESSION= CLAUDE_PROJECT_DIR=$ROOT}"
|
||||
N=$((N+1)); local err rc
|
||||
err=$(printf '%s' "$(post "$prompt" "$tool")" | env $envs bash "$HOOK" 2>&1 >/dev/null); rc=$?
|
||||
LAST_ERR="$err"
|
||||
if [ "$rc" -eq "$want" ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
|
||||
else printf ' ❌ %s —— 期望 exit=%s,實得 exit=%s\n' "$desc" "$want" "$rc"; printf '%s\n' "$err" | sed -n '1,8p' | sed 's/^/ /'; FAIL=$((FAIL+1)); fi
|
||||
}
|
||||
WT901="$REPO/.worktrees/shared-repo-901"
|
||||
# (a) 沒推:分身裡多一筆 commit、沒 push
|
||||
echo g > "$WT901/g.txt"; git -C "$WT901" add -A; git -C "$WT901" -c user.email=t@t -c user.name=t commit -qm g
|
||||
tp 2 "(86) 線收工、分支沒推 ⇒ exit 2(點名,不靜默)" "【工單】inkstone/shared-repo#901"
|
||||
has "feat/901" "(87) 點得出分支名"
|
||||
has "$WT901" "(88) 點得出路徑"
|
||||
has "push origin feat/901" "(89) 印得出推的指令"
|
||||
has "worktree-placement.md" "(90) 指到規約"
|
||||
N=$((N+1)); if [ -d "$WT901" ]; then printf ' ✅ (91) 沒推的分身一根寒毛都沒動(不刪沒推的 commit)\n'; PASS=$((PASS+1)); else printf ' ❌ (91) 沒推的分身被刪了——那是災難\n'; FAIL=$((FAIL+1)); fi
|
||||
# (b) 推了但工作樹髒:有未追蹤檔
|
||||
git -C "$WT901" push -q origin feat/901 2>/dev/null
|
||||
echo tmp > "$WT901/untracked.txt"
|
||||
tp 2 "(92) 推了、但分身裡有未追蹤檔 ⇒ exit 2 點名(git 自己也拒絕 remove)" "【工單】inkstone/shared-repo#901"
|
||||
has "未 commit" "(93) 講得出是有東西沒交代"
|
||||
N=$((N+1)); if [ -d "$WT901" ] && [ -f "$WT901/untracked.txt" ]; then printf ' ✅ (94) 髒的分身沒被動\n'; PASS=$((PASS+1)); else printf ' ❌ (94) 髒的分身被動了\n'; FAIL=$((FAIL+1)); fi
|
||||
# (c) 推了且乾淨 ⇒ 收掉、安靜
|
||||
rm -f "$WT901/untracked.txt"
|
||||
tp 0 "(95) 推了且乾淨 ⇒ exit 0(收掉)" "【工單】inkstone/shared-repo#901"
|
||||
N=$((N+1)); if [ ! -e "$WT901" ] && ! git -C "$REPO" worktree list | grep -q shared-repo-901; then printf ' ✅ (96) 分身真的收掉了(目錄與登記都沒了)\n'; PASS=$((PASS+1)); else printf ' ❌ (96) 分身還在\n'; FAIL=$((FAIL+1)); fi
|
||||
N=$((N+1)); if git -C "$REPO" rev-parse --verify -q refs/heads/feat/901 >/dev/null; then printf ' ✅ (97) 分支本身還在(收的是分身,不是分支)\n'; PASS=$((PASS+1)); else printf ' ❌ (97) 分支被刪了\n'; FAIL=$((FAIL+1)); fi
|
||||
N=$((N+1)); if [ -d "$OWN" ] && [ -d "$REPO2" ]; then printf ' ✅ (98) 別人的 worktree 與別的 repo 一根寒毛都沒動\n'; PASS=$((PASS+1)); else printf ' ❌ (98) 收錯東西了\n'; FAIL=$((FAIL+1)); fi
|
||||
# (d) 安靜的方向(誤攔比漏擋嚴重)
|
||||
tp 0 "(99) 同一張票再收一次(分身已不在)⇒ exit 0 安靜" "【工單】inkstone/shared-repo#901"
|
||||
hasnt "沒有找到" "(100) 沒分身時不吵(--quiet)"
|
||||
tp 0 "(101) 派工單沒有【工單】⇒ 不管" "幫我看一下 shared-repo#901 的狀況"
|
||||
tp 0 "(102) 票的 repo 沒有任何分身 ⇒ 安靜" "【工單】inkstone/other-repo#5"
|
||||
tp 0 "(103) PostToolUse 但工具不是 Agent/Task ⇒ 不管" "【工單】inkstone/shared-repo#901" "Bash"
|
||||
# (e) 一張派工單兩張票:兩份分身各自收
|
||||
(cd "$PLAIN" && python3 "$(cd "$HERE/../.." && pwd)/scripts/worktree" open inkstone/shared-repo#903 --repo "$REPO" >/dev/null 2>&1)
|
||||
(cd "$PLAIN" && python3 "$(cd "$HERE/../.." && pwd)/scripts/worktree" open inkstone/shared-repo#904 --repo "$REPO" >/dev/null 2>&1)
|
||||
echo k > "$REPO/.worktrees/shared-repo-904/k"; git -C "$REPO/.worktrees/shared-repo-904" add -A; git -C "$REPO/.worktrees/shared-repo-904" -c user.email=t@t -c user.name=t commit -qm k
|
||||
tp 2 "(104) 兩張工單:903 推了(HEAD=main 已在遠端)收掉、904 沒推點名 ⇒ exit 2" "【工單】inkstone/shared-repo#903
|
||||
【工單】inkstone/shared-repo#904"
|
||||
N=$((N+1)); if [ ! -e "$REPO/.worktrees/shared-repo-903" ] && [ -d "$REPO/.worktrees/shared-repo-904" ]; then printf ' ✅ (105) 903 收了、904 留著\n'; PASS=$((PASS+1)); else printf ' ❌ (105) 903/904 處理錯:903=%s 904=%s\n' "$([ -e "$REPO/.worktrees/shared-repo-903" ] && echo 在 || echo 沒了)" "$([ -e "$REPO/.worktrees/shared-repo-904" ] && echo 在 || echo 沒了)"; FAIL=$((FAIL+1)); fi
|
||||
has "shared-repo-904" "(106) 點名的是 904"
|
||||
# (f) 總管 session 的環境變數不影響收工那一半
|
||||
tp 2 "(107) 環境裡有 WORKTREE_OK=1 也照樣收工檢查(那是切分支的逃生門,不是收工的)" "【工單】inkstone/shared-repo#904" "Agent" "CLAUDE_CODE_CHILD_SESSION= CLAUDE_PROJECT_DIR=$ROOT WORKTREE_OK=1"
|
||||
|
||||
clean
|
||||
echo
|
||||
echo "══ $PASS/$N 通過${FAIL:+,$FAIL 個失敗} ══"
|
||||
|
||||
Reference in New Issue
Block a user