fix: 三支閘印出來的 ticket 一律絕對路徑,不叫到 InkStoneCo 舊複本(inkstone/ISEP#113)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TGxitYq49FzYC7EFkbhzF5
This commit is contained in:
2026-09-18 17:45:46 +00:00
parent 8a3802c4b7
commit ebe71e3283
6 changed files with 65 additions and 9 deletions
+7 -2
View File
@@ -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-guardinkstone/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 <owner/repo#N> -F <檔>`
`"__TICKET__" say <owner/repo#N> -F <檔>`
然後派工單寫 `【工單】<owner/repo#N> → 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 "<session_id>")
+ "\n"
)
+7 -3
View File
@@ -82,6 +82,10 @@
set -eu
PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# 訊息裡的出路一律印絕對路徑:收工方的 cwd 常是 InkStoneCo,那裡的 scripts/ticket 是
# 2026-08-27 之前的舊複本,照著相對路徑貼會叫到它(同 comment-carries-task-guard ⑮、
# baton-handback-guardinkstone/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**:這個回合直接呼叫 AgentTask 派出去。
沒有票 → 先 \`scripts/ticket where <關鍵字>\` 搜該掛哪張,
再 \`ticket say <owner/repo#N> -F <檔>\`,然後帶【工單】派工。
沒有票 → 先 \`"$TICKET" where <關鍵字>\` 搜該掛哪張,
再 \`"$TICKET" say <owner/repo#N> -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)⇒ **那是要重派的訊號,不是完工**
+9 -3
View File
@@ -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/ticketISEP#113\n'; PASS=$((PASS+1))
else
printf ' ❌ ⑰ 訊息被 shell 展開了(指令消失,或冒出 shell 錯誤\n'
printf ' ❌ ⑰ 訊息被 shell 展開,或出路又印回相對路徑的 scripts/ticketInkStoneCo 那份是舊複本\n'
printf '%s\n' "$msg" | sed -n '1,20p' | sed 's/^/ /'
FAIL=$((FAIL+1))
fi
+29
View File
@@ -181,6 +181,35 @@ echo "── 出路③ 的反面:沒寫標記就照樣擋(不准靠刪掉那
t 2 "同一句話拿掉 ⏸ 等: 標記 ⇒ 擋" \
'T:那批 workflow 要重打 bundle。我等你確認,這件事我不自己動。'
echo
echo "── 出路裡的 ticket 是絕對路徑,不是相對的 scripts/ticketinkstone/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)────────────────────────────────"