現在的主線是哪一個,變成一個查得到的事實(inkstone/ISEP#82)

14 個 open milestone 同時亮著,其中「Mira 現代化」同名活在 5 個 repo,
所以 SOP 說的「那個 active milestone」在現場沒有指涉對象。

- hooks/lib/mainline.py        主線的唯一存放處(一個檔放得下一條),從不打網路
- scripts/mainline             show/list/set/clear/refresh/adopt/has
- hooks/mainline-focus-guard.sh 派了不在主線上的票 ⇒ 攔一次,問補收還是跳線
- hooks/lib/countdown.py       ⏱ 那一行的主線改讀「被標定的」,蓋過「期限最近」的猜測
- hooks/countdown-guard.sh     同一個注入點加第二行 🎯(ISEP#63 那半不動)
- 測試 24 條(離線)+ countdown 原有 20 條仍全綠
This commit is contained in:
Claude Code
2026-08-28 00:30:09 +00:00
committed by Claude
parent bebbbd2c11
commit ac2f89c610
7 changed files with 1020 additions and 12 deletions
+8 -3
View File
@@ -145,12 +145,17 @@ case "$CTX" in
esac
# milestone:沒有快取就整段不出現(不准編一個日期出來)
case "$CTX" in
*"主線"*) no "⑨ 沒有 milestone 快取 ⇒ 那一段不出現" "實得:$CTX" ;;
# 🔴 只比對**第一行**(那條 ⏱ 倒數本身)。inkstone/ISEP#82 之後,同一個注入點還帶著
# 第二行「🎯 主線……」與一段說明,那段文字裡本來就會出現「主線」兩個字
# ——拿整段去比對會把說明當成數字,變成一條**測不到重點的假紅**。
# 這一格要驗的一直都是「⏱ 那一行有沒有多長出一段編出來的期限」。
STAMP_LINE_ONLY=$(printf '%s\n' "$CTX" | head -1)
case "$STAMP_LINE_ONLY" in
*"主線"*) no "⑨ 沒有 milestone 快取 ⇒ 那一段不出現" "實得:$STAMP_LINE_ONLY" ;;
*) ok "⑨ 沒有 milestone 快取 ⇒ 那一段不出現(不編數字)" ;;
esac
printf '2026-08-30|Sprint 8-28\n' > "$ISEP_COUNTDOWN_STATE_DIR/milestone-due"
CTX=$(inject s-ms "$TMP/a5.jsonl" "$T0")
CTX=$(printf '%s\n' "$(inject s-ms "$TMP/a5.jsonl" "$T0")" | head -1)
case "$CTX" in
*"主線 Sprint 8-28 剩"*) ok "⑩ 有 milestone 快取 ⇒ 期限那一段出現" ;;
*) no "⑩ 有 milestone 快取 ⇒ 期限那一段出現" "實得:$CTX" ;;
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
# 主線那一條線的迴歸測試(inkstone/ISEP#82
#
# 票上四條驗收,這裡一條一群:
# A 「現在的主線是哪一個」→ 一個指令答得出來,而且答案唯一
# B 標了主線之後開新回合 → 目標宣告自己出現(不用人去翻)
# C 派一張不屬於主線的票 → 攔一次,並問是補收還是跳線
# D 沒有任何主線 → 不能整組壞掉,要講得出「現在沒有主線」
# E 不該擋的(誤攔比漏擋更該修)
#
# 用法:hooks/tests/mainline-focus-guard.test.sh
# 🔴 全程離線:狀態走 ISEP_COUNTDOWN_STATE_DIR、時鐘走 ISEP_COUNTDOWN_NOW、
# 「那張票掛在哪」走 ISEP_MAINLINE_FIXTURE,並把 TICKET_HOST 指到一個
# 連不上的位址——**任何一條真的走到網路,就會在那裡當場失敗,不會靜靜地變成假綠**。
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GUARD="$ROOT/mainline-focus-guard.sh"
COUNTDOWN="$ROOT/countdown-guard.sh"
MAINLINE="$ROOT/../scripts/mainline"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
export ISEP_COUNTDOWN_STATE_DIR="$TMP/state"
export TICKET_HOST="http://127.0.0.1:9" # 連不上:走到網路就會馬上失敗
mkdir -p "$ISEP_COUNTDOWN_STATE_DIR"
unset ISEP_MAINLINE_FIXTURE
PASS=0; FAIL=0
ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
no(){ printf ' ❌ %s —— %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
T0=$(python3 -c 'import datetime as d;print(int(d.datetime(2026,8,28,0,0,tzinfo=d.timezone.utc).timestamp()))')
# set_mainline <標題> <成員 ref...> —— 直接寫 state(離線;`mainline set` 那半要網路)
set_mainline(){
python3 - "$ISEP_COUNTDOWN_STATE_DIR/mainline.json" "$@" <<'PY'
import json, sys
path, title = sys.argv[1], sys.argv[2]
json.dump({"ref": "inkstone/ISEP#57", "owner": "inkstone", "repo": "ISEP", "id": 57,
"title": title, "description": "把 SOP 變成閘:規則要長在機器上,不是長在誰的記性上。",
"due_on": "2026-08-31T15:59:59+08:00",
"open_issues": 12, "closed_issues": 1,
"members": list(sys.argv[3:]), "set_at": "2026-08-28 08:00"},
open(path, "w"), ensure_ascii=False)
PY
}
clear_mainline(){ rm -f "$ISEP_COUNTDOWN_STATE_DIR/mainline.json"; }
# dispatch <派工單內文> → "exit|訊息"
dispatch(){
out=$(python3 -c '
import json,sys; print(json.dumps({"tool_input":{"prompt":sys.argv[1]},"session_id":"t"}))' "$1" \
| CLAUDE_CODE_CHILD_SESSION= bash "$GUARD" 2>&1); rc=$?
printf '%s|%s' "$rc" "$(printf '%s' "$out" | tr '\n' ' ')"
}
# inject <now> → UserPromptSubmit 注入的內容
inject(){
printf '{"hook_event_name":"UserPromptSubmit","session_id":"s","transcript_path":"%s"}' "$TMP/none.jsonl" \
| ISEP_COUNTDOWN_NOW="$1" bash "$COUNTDOWN" 2>/dev/null \
| python3 -c 'import json,sys
try: print(json.load(sys.stdin)["hookSpecificOutput"]["additionalContext"])
except Exception: print("")'
}
echo "── A 群:「現在的主線是哪一個」一個指令答得出來,答案唯一(驗收 1)──"
clear_mainline
OUT=$(python3 "$MAINLINE" 2>&1); RC=$?
[ "$RC" = 0 ] && ok "① 沒標過主線 ⇒ 指令仍然回答得出來(exit 0,不是壞掉)" \
|| no "① 沒標過主線 ⇒ exit 0" "實得 exit=$RC"
case "$OUT" in
*"現在沒有主線"*) ok "② 沒標過主線 ⇒ 答案是「現在沒有主線」(驗收 4)" ;;
*) no "② 沒標過 ⇒ 說「現在沒有主線」" "實得:$OUT" ;;
esac
set_mainline "SOP 變成閘" "inkstone/ISEP#82" "inkstone/ISEP#57"
OUT=$(python3 "$MAINLINE" 2>&1)
N=$(printf '%s\n' "$OUT" | grep -c "現在的主線")
[ "$N" = 1 ] && ok "③ 標了之後 ⇒ 只有一個答案(「現在的主線」出現 1 次,不是 14 個)" \
|| no "③ 答案唯一" "「現在的主線」出現 $N"
case "$OUT" in
*"SOP 變成閘"*) ok "④ 答案指名道姓(標題印出來了)" ;;
*) no "④ 答案指名道姓" "實得:$OUT" ;;
esac
set_mainline "換一條線" "inkstone/ISEP#82"
# 🔴 只看標題那一行:描述欄裡本來就會提到舊標題(它是同一份測資),
# 拿整份輸出去比對會**因為描述而誤判**——那不是「舊的還是主線」。
OUT=$(python3 "$MAINLINE" 2>&1 | head -1)
case "$OUT" in
*"SOP 變成閘"*) no "⑤ 換標一條 ⇒ 舊的不再是主線" "舊標題還在:$OUT" ;;
*"換一條線"*) ok "⑤ 換標一條 ⇒ 舊的當場不再是主線(一個檔放得下一條)" ;;
*) no "⑤ 換標一條" "實得:$OUT" ;;
esac
echo "── B 群:標了之後開新回合,目標宣告自己出現(驗收 2)──────────"
set_mainline "SOP 變成閘" "inkstone/ISEP#82"
CTX=$(inject "$T0")
case "$CTX" in
*"🎯 主線:"*) ok "⑥ 什麼都沒交代 ⇒ 注入裡有 🎯 主線那一行" ;;
*) no "⑥ 注入裡有 🎯 主線那一行" "實得:$CTX" ;;
esac
case "$CTX" in
*"SOP 變成閘"*) ok "⑦ 注入裡有主線的名字" ;;
*) no "⑦ 注入裡有主線的名字" "實得:$CTX" ;;
esac
case "$CTX" in
*"目標:把 SOP 變成閘"*) ok "⑧ 注入裡有**目標宣告**(milestone 的描述),不用人去翻" ;;
*) no "⑧ 注入裡有目標宣告" "實得:$CTX" ;;
esac
# ⏱ 那一行是**已經有 Stop 閘在查**的那一行(ISEP#63)。主線的名字掛在它上面,
# 就跟著那道驗過的閘一起到 leo 眼前——不必為了同一件事再立第二道會擋人的閘。
FIRST=$(printf '%s\n' "$CTX" | head -1)
case "$FIRST" in
*"⏱"*"主線 SOP 變成閘"*) ok "⑨ 被查核的那一行(⏱)也帶著主線名字 ⇒ 主線會到 leo 眼前" ;;
*) no "⑨ ⏱ 那一行帶主線名字" "實得:$FIRST" ;;
esac
# 被標定的主線要蓋過「期限最近的那個」快取——後者是猜的,前者是被指定的
printf '2026-08-29|某個期限更近的\n' > "$ISEP_COUNTDOWN_STATE_DIR/milestone-due"
FIRST=$(inject "$T0" | head -1)
case "$FIRST" in
*"主線 SOP 變成閘"*) ok "⑩ 標定的主線蓋過「期限最近」的猜測(不准用猜的蓋過指定的)" ;;
*) no "⑩ 標定的主線優先" "實得:$FIRST" ;;
esac
rm -f "$ISEP_COUNTDOWN_STATE_DIR/milestone-due"
clear_mainline
CTX=$(inject "$T0")
case "$CTX" in
*"現在沒有主線"*) ok "⑪ 沒有主線 ⇒ 注入照樣講得出「現在沒有主線」(驗收 4,不編一條)" ;;
*) no "⑪ 沒有主線 ⇒ 注入講「現在沒有主線」" "實得:$CTX" ;;
esac
echo "── C 群:派了不在主線上的票 ⇒ 攔一次並問清楚(驗收 3)──────────"
set_mainline "SOP 變成閘" "inkstone/ISEP#82" "inkstone/ISEP#57"
printf '{"inkstone/Arcrun#50":"AI 問得到內文"}\n' > "$TMP/fx.json"
export ISEP_MAINLINE_FIXTURE="$TMP/fx.json"
R=$(dispatch '【工單】inkstone/ISEP#82')
[ "${R%%|*}" = 0 ] && ok "⑫ 派主線上的票 ⇒ 放行" || no "⑫ 派主線上的票 ⇒ 放行" "實得 exit=${R%%|*}"
R=$(dispatch '【工單】inkstone/Arcrun#50')
[ "${R%%|*}" = 2 ] && ok "⑬ 派不在主線上的票 ⇒ 擋" || no "⑬ 派不在主線上的票 ⇒ 擋" "實得 exit=${R%%|*}"
case "${R#*|}" in
*"補收"*"跳線"*) ok "⑭ 擋下時問的是「補收還是跳線」,兩條路都給了指令" ;;
*) no "⑭ 訊息問補收/跳線" "實得:${R#*|}" ;;
esac
case "${R#*|}" in
*"SOP 變成閘"*) ok "⑮ 擋下時說得出現在的主線是誰(不是只說「你錯了」)" ;;
*) no "⑮ 訊息說出主線是誰" "實得:${R#*|}" ;;
esac
R=$(dispatch '【工單】inkstone/Arcrun#50')
[ "${R%%|*}" = 0 ] && ok "⑯ 同一張票重送 ⇒ 放行(至多擋一次,不鬼打牆)" \
|| no "⑯ 重送 ⇒ 放行" "實得 exit=${R%%|*}"
J=$(wc -l < "$ISEP_COUNTDOWN_STATE_DIR/mainline-jumps.jsonl" 2>/dev/null || echo 0)
[ "$J" -ge 1 ] && ok "⑰ 跳線留痕(mainline-jumps.jsonl 記了一筆)——不留痕的話跳線與專注長得一樣" \
|| no "⑰ 跳線留痕" "jumps 檔有 $J"
R=$(dispatch '【工單】inkstone/Arcrun#50
【工單】inkstone/ISEP#82')
[ "${R%%|*}" = 0 ] && ok "⑱ 多張票只要有一張在主線上 ⇒ 放行(那次派工有在推主線)" \
|| no "⑱ 多張票有一張在主線 ⇒ 放行" "實得 exit=${R%%|*}"
echo "── D/E 群:不該擋(誤攔比漏擋更該修)──────────────────────────"
clear_mainline
R=$(dispatch '【工單】inkstone/Arcrun#50')
[ "${R%%|*}" = 0 ] && ok "⑲ 沒有主線 ⇒ 一律放行(驗收 4:不能整組壞掉)" \
|| no "⑲ 沒有主線 ⇒ 放行" "實得 exit=${R%%|*}"
set_mainline "SOP 變成閘" "inkstone/ISEP#82"
R=$(dispatch '幫我看一下這個 repo 的狀況')
[ "${R%%|*}" = 0 ] && ok "⑳ 派工單裡沒有票號 ⇒ 放行(那是 no-ticket-no-dispatch 的地盤)" \
|| no "⑳ 沒有票號 ⇒ 放行" "實得 exit=${R%%|*}"
out=$(python3 -c '
import json;print(json.dumps({"tool_input":{"prompt":"【工單】inkstone/Arcrun#50"},"session_id":"t"}))' \
| CLAUDE_CODE_CHILD_SESSION=1 bash "$GUARD" 2>&1); rc=$?
[ "$rc" = 0 ] && ok "㉑ 子 session ⇒ 放行(主線是派工者手上的判準,不是收工方要背的)" \
|| no "㉑ 子 session ⇒ 放行" "實得 exit=$rc"
unset ISEP_MAINLINE_FIXTURE
R=$(dispatch '【工單】inkstone/mira#999')
[ "${R%%|*}" = 0 ] && ok "㉒ 問不到那張票掛在哪(沒 fixture、網路也不通)⇒ 放行:讀不到 ≠ 不屬於" \
|| no "㉒ 問不到 ⇒ 放行" "實得 exit=${R%%|*}"
printf 'not json {{{\n' > "$ISEP_COUNTDOWN_STATE_DIR/mainline.json"
R=$(dispatch '【工單】inkstone/Arcrun#50')
[ "${R%%|*}" = 0 ] && ok "㉓ state 檔壞掉 ⇒ 放行(節拍器壞掉不該讓派工停擺)" \
|| no "㉓ state 壞掉 ⇒ 放行" "實得 exit=${R%%|*}"
OUT=$(python3 "$MAINLINE" 2>&1); RC=$?
[ "$RC" = 0 ] && ok "㉔ state 檔壞掉 ⇒ 指令仍然回答得出來(當成「現在沒有主線」)" \
|| no "㉔ state 壞掉 ⇒ 指令不崩" "實得 exit=$RC$OUT"
echo
echo "結果:通過 $PASS 條,失敗 $FAIL"
[ "$FAIL" -eq 0 ] || exit 1