diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index beb0c70..16f4d8f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "isep", "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:43 支機械閘(53 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、27 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。", - "version": "0.3.7", + "version": "0.3.8", "keywords": [ "inkstone", "guardrails", diff --git a/hooks/factory-idle-guard.sh b/hooks/factory-idle-guard.sh index 6581034..952e3b2 100755 --- a/hooks/factory-idle-guard.sh +++ b/hooks/factory-idle-guard.sh @@ -155,23 +155,27 @@ if not raw.strip(): if re.search(r"⏸\s*等\s*[::]\s*\S{2,}", raw): print("OK:waiting-declared"); raise SystemExit -# 出路②:票號指涉(=已經有人在做)。同樣在剝標記之前——票號常寫在行內 code 裡 -if re.search(r"#\d{1,5}", raw): - print("OK:ticket-referenced"); raise SystemExit - -# ── 把「引用性內容」換成哨兵:引用 != 主張 ──────────────────────── -# 換成哨兵而不是刪掉,是為了同時修好兩個方向: -# 內層的宣告消失(不再誤攔引用)+ 外層的句構留著(「回『X』我就…」照樣攔得到) -S = "※" # ※ +# ── 把「引用性內容」蓋掉:引用 != 主張 ────────────────────────── +# 🔴 **等長**替換(蓋成同樣長度的哨兵),不是刪掉,有兩個理由: +# ① 刪掉會把外層句構攪碎——2026-08-17 就是這樣漏掉「回『規劃』我就派人」。 +# 蓋成哨兵則內層的宣告消失、外層的句構留著,誤攔與漏攔一起修好。 +# ② 等長 ⇒ 位移可以原封不動對回 raw ⇒ 底下的「票號要在宣告句附近」 +# 可以在**原文**上量。否則票號寫在行內 `code` 裡會被自己剝掉而找不到, +# 逼得只能全篇搜——那會把閘變鈍(2026-08-23 拿真 transcript 量到: +# 全篇搜的話 26 個真實回合有 20 個是靠「文中某處有票號」放行的)。 +S = "※" +def blank(m): + return S * (m.end() - m.start()) text = raw -text = re.sub(r"```.*?```", S, text, flags=re.S) # markdown 圍欄 code -text = re.sub(r"~~~.*?~~~", S, text, flags=re.S) -text = re.sub(r"`[^`\n]*`", S, text) # 行內 code -text = re.sub(r"^\s*>.*$", "", text, flags=re.M) # 整段引言(leo 的原話) -for _ in range(3): # 引號(含巢狀),由內而外 - text = re.sub(r"[「『][^「」『』]*[」』]", S, text) +text = re.sub(r"```.*?```", blank, text, flags=re.S) # markdown 圍欄 code +text = re.sub(r"~~~.*?~~~", blank, text, flags=re.S) +text = re.sub(r"`[^`\n]*`", blank, text) # 行內 code +text = re.sub(r"^[ \t]*>.*$", blank, text, flags=re.M) # 整段引言(leo 的原話) +for _ in range(3): # 引號(含巢狀),由內而外 + text = re.sub(r"[「『][^「」『』]*[」』]", blank, text) # 本 repo 規定的 User Story 票名:是被命名的物件,不是我對下一步的主張 -text = re.sub(r"身為[^\n]{0,40}?我要[^\n]{0,80}?我才[^\n]{0,60}", S, text) +text = re.sub(r"身為[^\n]{0,40}?我要[^\n]{0,80}?我才[^\n]{0,60}", blank, text) +assert len(text) == len(raw) # 位移要能對回原文 # ── 宣告下一步的句型 ────────────────────────────────────────── # 🔴 每一條都**必須**接到一個動作動詞(A)才算命中。 @@ -179,7 +183,13 @@ text = re.sub(r"身為[^\n]{0,40}?我要[^\n]{0,80}?我才[^\n]{0,60}", S, text) A = r"(?:去|來|做|派|審|跑|查|補|建|寫|改|推|驗|清|落|盤|整理|處理|部署|測)" N = r"[^。!?;,,\n]{0,16}" # 同一句、不跨逗號 DECL = re.compile("|".join([ - r"下一步[::]?\s*(?:我|就是|是)?\s*" + N + A, + # 🔴 主詞是「你」的下一步 != 我的下一步(2026-08-23 拿真 transcript 量出來的第四類誤攔) + # 舊閘在 26 個真實回合終止點上擋了 2 次,兩次咬的都是我在**交代 leo 該做什麼**: + # 「**下一步還是那一個動作**:**你**把 feat/... 併進 main」 + # 「## **你下一步**(兩招,先便宜的)」 + # ⇒ 這是**主詞檢查**(誰要動手),不是關鍵字例外。 + # 只掛在這條 alternative 上——「你點頭我就做」那條的主詞本來就是我,不受影響。 + r"(?P下一步[::]?\s*(?!你|妳|leo|Leo)(?:我|就是|是)?\s*" + N + A + r")", r"接下來我\s*" + N + A, r"我(?:現在|接著|等下|等一下)(?:就)?\s*" + N + A, r"我(?:要|會)(?:去|來)?\s*" + N + A, @@ -198,11 +208,25 @@ DECL = re.compile("|".join([ r"我(?:就)?(?:一次)?(?:落|派|做|補|審|清)(?:完|掉)?\s*[。,,]?\s*$", ])) -hit = DECL.search(text) +SECOND = re.compile(r"你|妳|leo|Leo") +hit = None +for m in DECL.finditer(text): + # 「下一步」這條要看主詞:前面 8 字裡出現第二人稱 ⇒ 那是**他的**下一步,不是我的 + if m.group("nextstep") and SECOND.search(text[max(0, m.start() - 8): m.start()]): + continue + hit = m + break + if not hit: print("OK:no-declaration"); raise SystemExit # 沒宣告 ⇒ 純對話回合,不擋 -print("DECLARED_NO_TRIGGER::" + text[max(0, hit.start()-60): hit.end()+120].replace("\n", " ")[:200]) +# 出路②:宣告句**附近**有票號指涉(=已經說明它在誰手上/哪張票) +# 在 raw 上量(等長替換保證位移對得回去)⇒ 票號寫在行內 code 裡也算數, +# 而「文章別處剛好提到某張票」不算——那不是在講這件事在誰手上。 +if re.search(r"#\d{1,5}", raw[max(0, hit.start() - 200): hit.end() + 400]): + print("OK:ticket-referenced"); raise SystemExit + +print("DECLARED_NO_TRIGGER::" + raw[max(0, hit.start()-60): hit.end()+120].replace("\n", " ")[:200]) ' 2>/dev/null || echo "SKIP:crash") STAMP=$(date "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "?") diff --git a/hooks/tests/factory-idle-guard.test.sh b/hooks/tests/factory-idle-guard.test.sh index 81d11c7..566d412 100755 --- a/hooks/tests/factory-idle-guard.test.sh +++ b/hooks/tests/factory-idle-guard.test.sh @@ -90,6 +90,22 @@ t 0 "引用 leo 的話(blockquote)含宣告字樣" \ t 0 "User Story 票名(身為…我要…我才…)不是宣告" \ 'T:我把票名寫成:身為總管,我要把散落的分支收掉整理好,我才不會每天重數一次。' +# ↓ 這兩向不是想出來的,是拿本機一條 2068 行的真 transcript 跑出來的: +# 舊閘在 26 個真實回合終止點上擋了 2 次,兩次咬的都是「我在交代 leo 該做什麼」。 +t 0 "④ 主詞是 leo 的下一步(『你下一步去把 X 併進 main』)" \ + 'T:## 你下一步(兩招,先便宜的) + +你下一步去把 feat/isep-dynamic-install 併進 main,然後開一個全新雲端 session。' + +t 0 "④ 同上,交代對方要跑的動作" \ + 'T:前三題不適用。下一步還是那一個動作:你把那條分支併進 main,再跑一次看信標印什麼。' + +t 0 "④ 『你的下一步』——主詞在對方身上(單字 lookbehind 蓋不到的變體)" \ + 'T:我把兩條路都寫清楚了。你的下一步去把那條分支併進 main,再開一個新 session 看信標。' + +t 2 "④ 的反面:主詞換回我自己 ⇒ 照樣擋" \ + 'T:我把兩條路都寫清楚了。我的下一步去把那條分支併掉。' + echo echo "── A 群:既有放行路徑,不准變鬆(迴歸)─────────────────────────" @@ -129,6 +145,16 @@ t 2 "『我現在就去跑一遍』" \ t 2 "『確認後我才動』" \ 'T:核准後我才去改那支閘。' +t 0 "票號寫在行內 code 裡也算數(等長替換才量得到)" \ + 'T:下一步我去把那支閘補好——已派給 `inkstone/ISEP#30`。' + +t 2 "票號離宣告句太遠(文章別處提到而已)⇒ 不算它在別人手上" \ + 'T:早上那批已經在 inkstone/ISEP#30 收掉了。 + +(中略:這裡是一大段跟那張票無關的驗收敘述,用來把距離拉開。'"$(printf 'x%.0s' $(seq 1 320))"') + +下一步我去把那支閘的誤攔補上。' + echo echo "── C 群:訊息承諾的每一條出路,都要真的走得通 ─────────────────"