From ebe71e328369869af345bb548661309b404b87ba Mon Sep 17 00:00:00 2001 From: isep-hand Date: Fri, 18 Sep 2026 17:45:46 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=89=E6=94=AF=E9=96=98=E5=8D=B0?= =?UTF-8?q?=E5=87=BA=E4=BE=86=E7=9A=84=20ticket=20=E4=B8=80=E5=BE=8B?= =?UTF-8?q?=E7=B5=95=E5=B0=8D=E8=B7=AF=E5=BE=91=EF=BC=8C=E4=B8=8D=E5=8F=AB?= =?UTF-8?q?=E5=88=B0=20InkStoneCo=20=E8=88=8A=E8=A4=87=E6=9C=AC=EF=BC=88in?= =?UTF-8?q?kstone/ISEP#113=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dispatch-format-guard.sh 的出路 `scripts/ticket say`、factory-idle-guard.sh 主閘與 副閘的 `scripts/ticket where` 原本印相對路徑。相對路徑是對貼上去那個人的 cwd 解析的, 而收工方 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是 2026-08-27 之前的舊複本 (grep -c subtask → 0)⇒ 照著貼會叫到舊的、什麼都不會發生(同 ISEP#112 的病)。 兩支都改成從 ${CLAUDE_PLUGIN_ROOT} 解出絕對路徑再印(沿用 baton-handback-guard 的做法; baton 本輪未動,早已是絕對路徑)。dispatch 用既有的佔位符替換機制加 __TICKET__; factory 是 POSIX sh、unquoted heredoc,直接展開 $TICKET,並用 baton 的引號慣例包住路徑。 測試各加一條在驗這件事: - dispatch-format-guard.test.sh ⑰ 改成驗絕對路徑+確認沒有相對的 scripts/ticket(52/52) - factory-idle-guard.test.sh 加主閘訊息實跑 + 掃全支源碼兩條(33→35) - baton 既有 24/24 不動 版本待總管定(plugin.json 未動)。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TGxitYq49FzYC7EFkbhzF5 --- docs/TESTING.md | 5 +++- docs/hooks-inventory.md | 9 +++++++ hooks/dispatch-format-guard.sh | 9 +++++-- hooks/factory-idle-guard.sh | 10 +++++--- hooks/tests/dispatch-format-guard.test.sh | 12 +++++++--- hooks/tests/factory-idle-guard.test.sh | 29 +++++++++++++++++++++++ 6 files changed, 65 insertions(+), 9 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 3070053..a1d52a7 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1186,7 +1186,10 @@ bash hooks/tests/dispatch-format-guard.test.sh 交件方式、不准 push main、org 是 `inkstone` 這些不必有人記得寫,機器每次都補。 它壞了不會有人立刻發現——派工照樣送出去,只是收工方**不知道要貼回原票** - ⑨ 紅 ⇒ 真跡放行了。那份測資是**真的發生過的那一次派工**(見 `hooks/tests/fixtures/README.md`) -- ⑰ 紅 ⇒ 訊息被 shell 展開了(同 A9 ⑩b 那個病:閘照擋,但它教人怎麼解的那兩行變成空白) +- ⑰ 紅 ⇒ 訊息被 shell 展開了(同 A9 ⑩b 那個病:閘照擋,但它教人怎麼解的那兩行變成空白), + **或出路又印回相對路徑的 `scripts/ticket`**(`inkstone/ISEP#113`):收工方 cwd 常是 + InkStoneCo,那裡的 `scripts/ticket` 是 2026-08-27 之前的舊複本,相對路徑貼過去會叫到它、 + 什麼都不會發生——同 comment-carries ⑮、`baton-handback-guard`。出路要印**絕對路徑**的 ticket - **D 群⑲⑳㉑㉒ 紅 ⇒ `inkstone/ISEP#88` 的驗收條件沒過**:回覆一個正在跑的 subagent 時夾帶指令沒被擋(⑲)、或擋了卻沒把那段內容原文印出來(㉑,出路是「貼上票」, 找不到原文就得回頭自己翻) diff --git a/docs/hooks-inventory.md b/docs/hooks-inventory.md index 69efa14..f136dae 100644 --- a/docs/hooks-inventory.md +++ b/docs/hooks-inventory.md @@ -11,6 +11,15 @@ 其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。 下面按「你會在什麼時候撞到它」分組,一支一行。 +> 📌 **`inkstone/ISEP#113`(2026-09-18,待總管定版)+0 支、+0 條、腳本不變**:沒加閘、沒加腳本。 +> `dispatch-format-guard.sh`(出路 `scripts/ticket say`)與 `factory-idle-guard.sh`(主閘+副閘的 `scripts/ticket where`) +> 印出來的 ticket 指令改成**絕對路徑**(`${CLAUDE_PLUGIN_ROOT}/scripts/ticket`)——收工方 cwd 常是 InkStoneCo, +> 那裡的 `scripts/ticket` 是 2026-08-27 之前的舊複本,相對路徑貼過去會叫到它、什麼都不會發生 +> (同 `comment-carries-task-guard` ⑮/`baton-handback-guard`,`inkstone/ISEP#112`)。`baton-handback-guard` 早已是絕對路徑,本輪未動。 +> 兩支的測試各加一條在驗這件事:`dispatch-format-guard.test.sh` ⑰(52/52)、`factory-idle-guard.test.sh`(33→35)。 +> **62 支、88 條、66 支腳本是在這棵樹上當場數出來的**(`ls hooks/*.sh | wc -l`=62、 +> `grep -c '"command":' hooks/hooks.json`=88、`ls -p scripts | grep -v / | wc -l`=66)。 + > 📌 **`inkstone/ISEP#159`(2026-09-18,待總管定版)+0 支、+0 條、腳本 65→66**:沒加閘。`scripts/ticket` 多兩個動詞 > `triage`(沒 s/* 的票給狀態+留說明,或當重複關掉並指回去)與 `compose`(把已分診的票組成 InkStoneCo 里程碑:載體+前置, > 先全部驗過才寫);`pick --pool` 多兩層(還沒到期的 InkStoneCo 里程碑的前置、最後是待分診的票),每一列帶 `next`。 diff --git a/hooks/dispatch-format-guard.sh b/hooks/dispatch-format-guard.sh index a1bcdb1..9c42d97 100755 --- a/hooks/dispatch-format-guard.sh +++ b/hooks/dispatch-format-guard.sh @@ -83,6 +83,10 @@ set -u INPUT="$(cat)" HERE="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +# 訊息裡的出路一律印絕對路徑:收工方的 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是 +# 2026-08-27 之前的舊複本,照著相對路徑貼會叫到它(同 comment-carries-task-guard ⑮、 +# baton-handback-guard;inkstone/ISEP#112/#113)。 +TICKET="${CLAUDE_PLUGIN_ROOT:-$(cd "$HERE/.." && pwd)}/scripts/ticket" FACTS="$(printf '%s' "$INPUT" | python3 "$HERE/lib/dispatch_parse.py" dispatch 2>/dev/null)" [ -n "$FACTS" ] || exit 0 # parser 掛了 → fail-open(回到「沒有這支閘」的狀態) @@ -151,7 +155,7 @@ __CARRY__ **本閘每次派工都會自動注入給收工方**,你不必寫、也不會漏。 只有這次 ⇒ **寫進那張票**(main 是哪顆、今天撞過什麼、另一條線在動什麼) - `scripts/ticket say -F <檔>` + `"__TICKET__" say -F <檔>` 然後派工單寫 `【工單】 → comment <剛拿到的號碼>` 🔴 **「票上還沒有」不是把它寫進 prompt 的理由——它就是「去把它寫上票」的指令。** @@ -207,13 +211,14 @@ if t: fi DFG_MSG="$MSG" DFG_VIOL="$VIOL_TEXT" DFG_SESSION="$SESSION" \ - DFG_HEAD="$HEADLINE" DFG_CARRY="$CARRY_TEXT" python3 -c ' + DFG_HEAD="$HEADLINE" DFG_CARRY="$CARRY_TEXT" DFG_TICKET="$TICKET" python3 -c ' import os, sys sys.stderr.write( os.environ["DFG_MSG"] .replace("__HEADLINE__", os.environ.get("DFG_HEAD", "")) .replace("__VIOLATIONS__", os.environ.get("DFG_VIOL", "")) .replace("__CARRY__", os.environ.get("DFG_CARRY", "")) + .replace("__TICKET__", os.environ.get("DFG_TICKET", "") or "scripts/ticket") .replace("__SESSION__", os.environ.get("DFG_SESSION", "") or "") + "\n" ) diff --git a/hooks/factory-idle-guard.sh b/hooks/factory-idle-guard.sh index 952e3b2..d933c21 100755 --- a/hooks/factory-idle-guard.sh +++ b/hooks/factory-idle-guard.sh @@ -82,6 +82,10 @@ set -eu PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}" +# 訊息裡的出路一律印絕對路徑:收工方的 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是 +# 2026-08-27 之前的舊複本,照著相對路徑貼會叫到它(同 comment-carries-task-guard ⑮、 +# baton-handback-guard;inkstone/ISEP#112/#113)。$0 是本檔路徑,往上一層就是 plugin 根。 +TICKET="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}/scripts/ticket" PAYLOAD=$(cat 2>/dev/null || echo '{}') # ── 主閘:宣告了下一步,有沒有交出派工憑證?──────────────────────── @@ -263,8 +267,8 @@ leo 2026-08-16(本閘的規格): ━━ 現在怎麼過這道閘(四條,程式碼裡都真的有對應的分支)━━━━━━━━━ ① **現在就按 trigger**:這個回合直接呼叫 Agent/Task 派出去。 - 沒有票 → 先 \`scripts/ticket where <關鍵字>\` 搜該掛哪張, - 再 \`ticket say -F <檔>\`,然後帶【工單】派工。 + 沒有票 → 先 \`"$TICKET" where <關鍵字>\` 搜該掛哪張, + 再 \`"$TICKET" say -F <檔>\`,然後帶【工單】派工。 ② **它已經在別人手上**:把票號寫進這段收尾文字(例:「已派給 inkstone/ISEP#30」)。 有 \`#數字\` 就放行——那不是「我等下做」,是「已經有人在做」。 ③ **它其實不該現在做**:寫一行說明它在等什麼,格式就是這一個標記—— @@ -318,7 +322,7 @@ leo 2026-08-10:「你的任務是**維護 loop**,你要去拿任務派任務 **全工廠停工你要發現,這是警訊**。」 【現在該做的,不是回報,是派工】 - 1. 撈池子當場撈:\`scripts/ticket where <關鍵字>\`(或 labels=s/todo) + 1. 撈池子當場撈:\`"$TICKET" where <關鍵字>\`(或 labels=s/todo) 2. 照 CP 排序:\`system-dev/docs/3-specs/critical-paths/ship.md\` 是有序的六步 3. 一次派 2–4 件平行跑;同一個 repo 不要塞兩條(會搶工作區) 4. agent 死於環境錯誤(SSL/API)⇒ **那是要重派的訊號,不是完工** diff --git a/hooks/tests/dispatch-format-guard.test.sh b/hooks/tests/dispatch-format-guard.test.sh index 6549282..1503b96 100755 --- a/hooks/tests/dispatch-format-guard.test.sh +++ b/hooks/tests/dispatch-format-guard.test.sh @@ -216,13 +216,19 @@ fi # 迴歸:訊息裡的反引號一度在別支閘被 shell 當成命令替換執行掉 # (2026-08-26 ask-user-question-guard:閘照擋,但**它教人怎麼解的那兩行變成空白**) +# ⑰ 兩件事一起驗(都會讓「照著貼」失敗): +# ① 反引號沒被 shell 當命令替換執行掉(出路那兩行沒變空白)——原本這條的用意 +# ② 出路印的是**絕對路徑**的 ticket,不是相對的 `scripts/ticket`(inkstone/ISEP#113): +# 收工方 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是舊複本(`grep -c subtask` → 0) +# ⇒ 相對路徑貼過去會叫到舊的、什麼都不會發生(同 comment-carries ⑮、baton 那條) N=$((N+1)) -if printf '%s' "$msg" | grep -q 'scripts/ticket say' \ +if printf '%s' "$msg" | grep -qE '/scripts/ticket" say' \ + && ! printf '%s' "$msg" | grep -qE '`scripts/ticket' \ && printf '%s' "$msg" | grep -q 'touch /tmp/.dispatch-ok-S-TEST' \ && ! printf '%s' "$msg" | grep -qi 'is a directory\|command not found'; then - printf ' ✅ ⑰ 訊息原文照印:反引號沒被當命令執行,出路那兩行沒有變空白\n'; PASS=$((PASS+1)) + printf ' ✅ ⑰ 訊息原文照印(反引號沒被執行)+出路的 ticket 是絕對路徑,沒有相對的 scripts/ticket(ISEP#113)\n'; PASS=$((PASS+1)) else - printf ' ❌ ⑰ 訊息被 shell 展開了(指令消失,或冒出 shell 錯誤)\n' + printf ' ❌ ⑰ 訊息被 shell 展開,或出路又印回相對路徑的 scripts/ticket(InkStoneCo 那份是舊複本)\n' printf '%s\n' "$msg" | sed -n '1,20p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi diff --git a/hooks/tests/factory-idle-guard.test.sh b/hooks/tests/factory-idle-guard.test.sh index 566d412..e3e916a 100755 --- a/hooks/tests/factory-idle-guard.test.sh +++ b/hooks/tests/factory-idle-guard.test.sh @@ -181,6 +181,35 @@ echo "── 出路③ 的反面:沒寫標記就照樣擋(不准靠刪掉那 t 2 "同一句話拿掉 ⏸ 等: 標記 ⇒ 擋" \ 'T:那批 workflow 要重打 bundle。我等你確認,這件事我不自己動。' +echo +echo "── 出路裡的 ticket 是絕對路徑,不是相對的 scripts/ticket(inkstone/ISEP#113)─" +# 收工方的 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是 2026-08-27 之前的舊複本 +# (`grep -c subtask` → 0)⇒ 相對路徑貼過去會叫到它、什麼都不會發生(同 comment-carries ⑮、 +# baton-handback)。這兩條分別驗「主閘印出來的那塊」與「整支閘的訊息裡再也沒有相對路徑」。 +N=$((N+1)) +mk "$TMP/tr-path.jsonl" 'T:我看完了。下一步我去把那支閘的誤攔補上。' +pmsg=$(printf '{"transcript_path":"%s"}' "$TMP/tr-path.jsonl" \ + | CLAUDE_PROJECT_DIR="$TMP" sh "$HOOK" 2>&1) +if printf '%s' "$pmsg" | grep -qE '/scripts/ticket" where' \ + && ! printf '%s' "$pmsg" | grep -qE '`scripts/ticket'; then + printf ' ✅ 主閘出路的 ticket 印絕對路徑,沒有相對的 scripts/ticket\n'; PASS=$((PASS+1)) +else + printf ' ❌ 主閘出路又印回相對的 scripts/ticket ⇒ 在 InkStoneCo cwd 貼上去會叫到舊複本\n' + printf '%s\n' "$pmsg" | grep -n 'ticket' | sed 's/^/ /' + FAIL=$((FAIL+1)) +fi + +# 副閘(全廠停工那塊)在離線測試裡拿不到 gitea token、走不到,所以直接掃這支閘的原始碼: +# 印出來的每一處 ticket 指令都不准是相對路徑(backtick 直接接 scripts/ticket)。 +N=$((N+1)) +if grep -n '`scripts/ticket' "$HOOK" >/dev/null 2>&1; then + printf ' ❌ 這支閘的訊息裡還有相對路徑的 scripts/ticket(副閘那塊漏改了)\n' + grep -n '`scripts/ticket' "$HOOK" | sed 's/^/ /' + FAIL=$((FAIL+1)) +else + printf ' ✅ 整支閘的訊息裡沒有任何相對路徑的 scripts/ticket(主閘+副閘都改了)\n'; PASS=$((PASS+1)) +fi + echo echo "── 壞輸入不准亂擋(fail-open)────────────────────────────────"