一個 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:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# 管什麼: 一條派工線要在「共用的工作目錄」裡切分支時擋下來,叫它去開一份自己的 worktree。
|
||||
# 管什麼: 一條派工線要在「共用的工作目錄」裡切分支時擋下來,叫它去開一份自己的 worktree——開在 repo **裡面**(`.worktrees/`),不開在旁邊;同一支閘在那條線收工那一刻(PostToolUse Agent|Task)把分身收掉:分支推了就 remove,沒推就點名分支與路徑,不靜默留著。
|
||||
# 為什麼: 2026-08-29 inkstone/ISEP#109 comment 5391——每個 repo 只有一份工作目錄,所有線共用。#163 切到自己的分支、交回時沒切回來,總管後來在同一個目錄跑 ship.mjs,讀到的是別人留下的 HEAD。票號說得出「這是哪個任務」,沒有任何東西說得出「這個目錄現在是誰留下的狀態」。
|
||||
# 🔴 2026-09-07 inkstone/ISEP#147:第一版教工人「開在 repo 旁邊、收工 worktree remove」,而 remove 那一步只寫在閘訊息裡、沒有任何機制驗 ⇒ 每個工人留一份,leo 在 Finder 看到 `ISEP-wt-117`/`ISEP-wt115`/`InkStoneCo-wt-112`/`course_gen-ticket-3-notes`…一排同頭不同尾的分身。leo:「一個 repo 只看得到一個資料夾」「禁止這樣產出一大堆資料夾」。規則在文字裡不在機器上——所以現在兩端都在機器上:開在裡面(Finder 看不到),收在 PostToolUse(忘了也會被收)。工具=scripts/worktree(open/close/sweep 同一份「推了沒」判準);規約=docs/governance/worktree-placement.md。
|
||||
# 誤觸時怎麼關: `WORKTREE_OK=1 git checkout …`——認的是**指令字串裡**這個字面前綴(留痕就在指令歷史上)。
|
||||
# 🔴 2026-09-07(inkstone/ISEP#109 → comment 6587):舊版讀的是 hook 自己的環境變數 `$WORKTREE_OK`,
|
||||
# 而 PreToolUse hook 跟指令不是同一個行程——指令前綴的賦值到不了它 ⇒ 閘印出來的出路在 Claude Code
|
||||
@@ -31,16 +32,28 @@
|
||||
# 而 08-29 出事的是子 repo(`products/arcrun-rag`、`matrix/arcrun`)、動作是 `checkout`、
|
||||
# 而且沒有人登記過。**兩支守的不是同一件事,這支不動那支。**
|
||||
#
|
||||
# ── worktree 開在哪:開在 repo 的旁邊,不要開在 /tmp(2026-08-29 實測)──────
|
||||
# ── worktree 開在哪:repo **裡面**的 `.worktrees/`,不在旁邊、不在 /tmp ─────────
|
||||
# /tmp(2026-08-29 實測):
|
||||
# $ git -C products/arcrun-rag worktree list
|
||||
# /private/tmp/.../scratchpad/wt-180 … prunable ← 目錄被系統清掉了
|
||||
# /private/tmp/.../scratchpad/wt164 … prunable ← 同上
|
||||
# /private/tmp/wt167src … prunable
|
||||
# products/arcrun-rag-wt88 … 完好
|
||||
# products/arcrun-rag-wt104ingest … 完好
|
||||
# ISEP 同一天有 **12 筆 prunable**,全部在 /private/tmp 底下。
|
||||
# ⇒ 開在 `/tmp` 的會被系統清掉,而**登記簿還留著** ⇒ 登記簿說謊。
|
||||
# 開在 repo 旁邊的那幾份,一份都沒掉。
|
||||
# 旁邊(2026-09-07 inkstone/ISEP#147 實錄):一份都沒掉——**也一份都沒收**。
|
||||
# tech_projects/ 底下躺著 ISEP-wt-122ship、ISEP-wt-117、ISEP-wt115、InkStoneCo-wt-112…
|
||||
# leo 在 Finder 看到的是「一個 repo 長出一排同頭不同尾的分身」。
|
||||
# ⇒ 住 `<repo>/.worktrees/<票 repo>-<票號>`:Finder 看不到(點開頭)、`git status` 看不到
|
||||
# (`scripts/worktree open` 寫進 `.git/info/exclude`,不動被追蹤的檔)、
|
||||
# `git worktree list` 照樣列得到(收工那一半靠它找)。
|
||||
#
|
||||
# ── 收工那一半:同一支閘掛在 PostToolUse(Agent|Task)(inkstone/ISEP#147)───────
|
||||
# 派工單裡的 `【工單】owner/repo#N` 就是分身的名字(`.worktrees/<repo>-<N>`),
|
||||
# 跟 baton-handback-guard.sh/issue-status-autoflip.sh 同一個位置、同一種抓法。
|
||||
# 線收工那一刻:`scripts/worktree close <票>` ——
|
||||
# 推了且乾淨 ⇒ remove+prune,安靜。
|
||||
# 沒推/有未 commit/問不到遠端 ⇒ exit 2 點名分支與路徑,並印推的指令。**不 force、不刪。**
|
||||
# 為什麼不掛 SubagentStop:那裡拿不到票號(見 baton-handback-guard.sh 檔頭)。
|
||||
# 為什麼不另開一支閘:教你開的和替你收的是同一件事的兩半,拆成兩支就又是「規則在兩個地方」。
|
||||
#
|
||||
# ── 順手把說謊的登記清掉(驗收條件 7:prune 是收工動作,不是偶爾整理)───────
|
||||
# 擋下來的同時對那個 repo 跑一次 `git worktree prune`,並把清掉幾筆講出來。
|
||||
@@ -57,11 +70,69 @@
|
||||
# 然後以為自己隔離好了。**寧可漏擋,也不要給錯的下一步**(comment 5398)。
|
||||
set -eu
|
||||
|
||||
PAYLOAD=$(cat 2>/dev/null || echo '{}')
|
||||
|
||||
# ── 收工那一半(PostToolUse Agent|Task):先於任何環境變數判斷——這裡跑的是總管的 session ──
|
||||
EVENT=$(printf '%s' "$PAYLOAD" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except Exception:
|
||||
raise SystemExit
|
||||
sys.stdout.write(str(d.get("hook_event_name") or ""))
|
||||
' 2>/dev/null) || EVENT=""
|
||||
if [ "${EVENT:-}" = "PostToolUse" ]; then
|
||||
SPEC=$(printf '%s' "$PAYLOAD" | python3 -c '
|
||||
import json, re, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except Exception:
|
||||
raise SystemExit
|
||||
if d.get("tool_name") not in ("Agent", "Task"):
|
||||
raise SystemExit
|
||||
p = (d.get("tool_input") or {}).get("prompt") or ""
|
||||
seen = []
|
||||
for line in p.splitlines():
|
||||
if "【工單】" not in line:
|
||||
continue
|
||||
for m in re.finditer(r"([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#(\d+)", line):
|
||||
t = "%s/%s#%s" % m.groups()
|
||||
if t not in seen:
|
||||
seen.append(t)
|
||||
sys.stdout.write(" ".join(seen))
|
||||
' 2>/dev/null) || SPEC=""
|
||||
[ -n "${SPEC:-}" ] || exit 0
|
||||
WT_TOOL="$(cd "$(dirname "$0")/.." && pwd)/scripts/worktree"
|
||||
if [ ! -f "$WT_TOOL" ]; then
|
||||
echo "⚠️ line-needs-own-worktree:找不到 scripts/worktree,這條線的分身這一次沒有收。" >&2
|
||||
exit 0
|
||||
fi
|
||||
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
||||
# 掃哪裡:專案根、它的上一層(Mac:tech_projects/ 底下 ISEP 與 InkStoneCo 是兄弟)、
|
||||
# 雲端薄殼底下的真身(子 repo 在 InkStoneCo/matrix|products|polaris 底下,工具自己會往下找一層)。
|
||||
SCAN="--scan $ROOT --scan $(dirname "$ROOT")"
|
||||
[ -d "$ROOT/InkStoneCo" ] && SCAN="$SCAN --scan $ROOT/InkStoneCo"
|
||||
RC=0
|
||||
for T in $SPEC; do
|
||||
rc=0
|
||||
# shellcheck disable=SC2086
|
||||
OUT=$(python3 "$WT_TOOL" close "$T" $SCAN --quiet 2>&1 >/dev/null) || rc=$?
|
||||
[ -n "${OUT:-}" ] && printf '%s\n' "$OUT" | sed 's/^/ /' >&2
|
||||
[ "$rc" -eq 2 ] && RC=2
|
||||
done
|
||||
[ "$RC" -eq 2 ] || exit 0
|
||||
cat >&2 <<'EOF'
|
||||
|
||||
🌳 這條線收工了,但它的分身還留在 repo 裡(上面點名的那幾份)。
|
||||
不會靜默留著、也不會替它刪沒推的東西——推了再收,或確認作廢後自己刪。
|
||||
規約:docs/governance/worktree-placement.md(inkstone/ISEP#147)
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[ "${WORKTREE_OK:-}" = "1" ] && exit 0
|
||||
[ "${CLAUDE_CODE_CHILD_SESSION:-}" = "1" ] || exit 0 # 總管在自己的目錄裡,不管
|
||||
|
||||
PAYLOAD=$(cat 2>/dev/null || echo '{}')
|
||||
|
||||
# 這條指令會動到哪個目錄的 HEAD——**不是問「hook 站在哪」**。
|
||||
# 2026-08-29 實測(inkstone/ISEP#109 → comment 5398):`cd <別的 repo> && git checkout`
|
||||
# 舊版拿 payload 的 cwd 當答案,於是擋對了、卻**指著另一個 repo 叫人去開 worktree**。
|
||||
@@ -125,6 +196,8 @@ ABS() { (cd "$TARGET" 2>/dev/null && cd "$1" 2>/dev/null && pwd) || printf '%s'
|
||||
TOP=$(git -C "$TARGET" rev-parse --show-toplevel 2>/dev/null) || exit 0
|
||||
NAME=$(basename "$TOP")
|
||||
PARENT=$(dirname "$TOP")
|
||||
# 印給人複製的指令用工具的絕對路徑——相對路徑會對貼的人的 cwd 解析(mistakes.md「出路跑不動」)
|
||||
WT_TOOL="$(cd "$(dirname "$0")/.." && pwd)/scripts/worktree"
|
||||
NOW=$(git -C "$TOP" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')
|
||||
|
||||
# 驗收條件 7:說謊的登記順手清掉(只刪目錄已經不存在的那些)
|
||||
@@ -147,17 +220,20 @@ $PRUNED
|
||||
🔴 票號說得出「這是哪個任務」,**沒有任何東西說得出「這個目錄現在是誰留下的狀態」**。
|
||||
(那次沒出錯貨——是文案契約閘擋下來的。閘有效,但那是最後一道。)
|
||||
|
||||
【現在怎麼做】開一份自己的,然後在裡面做事:
|
||||
【現在怎麼做】開一份自己的——住在 repo **裡面**的 .worktrees/,然後在裡面做事:
|
||||
|
||||
git -C $TOP worktree add $PARENT/$NAME-wt-<票號> -b <你的分支>
|
||||
cd $PARENT/$NAME-wt-<票號>
|
||||
python3 $WT_TOOL open <owner/repo#票號> --repo $TOP -b <你的分支>
|
||||
cd $TOP/.worktrees/<票的 repo 名>-<票號>
|
||||
|
||||
🔴 **不要開在 /tmp 底下。** 同一天實測:$NAME 的登記簿裡指向 /private/tmp 的
|
||||
那幾筆全部 prunable(目錄被系統清掉、登記還在),而開在 repo 旁邊的一份都沒掉。
|
||||
🔴 **不要開在 /tmp 底下**(08-29 實測:$NAME 指向 /private/tmp 的登記全部 prunable),
|
||||
**也不要開在 repo 旁邊**(09-07 實錄:$PARENT/ 底下躺了一排 $NAME-wt-…,
|
||||
leo 在 Finder 看到「一個 repo 一排分身」——inkstone/ISEP#147,禁止)。
|
||||
裡面的那份 Finder 看不到、git status 看不到,git worktree list 找得到。
|
||||
|
||||
收工時:
|
||||
git -C $TOP worktree remove $PARENT/$NAME-wt-<票號>
|
||||
git -C $TOP worktree prune
|
||||
收工時(推了會收掉;沒推會點名,不會靜默留著;你忘了,線收工那一刻這支閘也會替你收):
|
||||
python3 $WT_TOOL close <owner/repo#票號> --repo $TOP
|
||||
|
||||
規約:docs/governance/worktree-placement.md
|
||||
|
||||
【真的要在共用目錄裡切(接手、緊急止血)】
|
||||
WORKTREE_OK=1 git -C $TOP checkout <分支>
|
||||
|
||||
Reference in New Issue
Block a user