From e3d05df34104442504c769a4ecec94d3216bdc26 Mon Sep 17 00:00:00 2001 From: richblack Date: Sun, 23 Aug 2026 17:54:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=8D=E4=BF=AE=E5=85=A9=E9=A1=9E=EF=BC=9A?= =?UTF-8?q?=E4=B8=BB=E8=A9=9E=E6=98=AF=20leo=20=E7=9A=84=E4=B8=8B=E4=B8=80?= =?UTF-8?q?=E6=AD=A5=E3=80=81=E7=A5=A8=E8=99=9F=E6=94=BE=E5=AF=AC=E6=8A=8A?= =?UTF-8?q?=E9=96=98=E8=AE=8A=E9=88=8D=E2=80=94=E2=80=94=E9=83=BD=E6=98=AF?= =?UTF-8?q?=E7=9C=9F=20transcript=20=E9=87=8F=E5=87=BA=E4=BE=86=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 交付警察擋回來是對的:前一顆只驗了「我自己造的假 transcript」。 改用本機一條 2068 行的真 session(26 個真實回合終止點)重驗,當場多找到兩個問題: ① 主詞是「你」的下一步,被當成我的宣告。 舊閘在 26 個真實回合裡擋了 2 次,兩次咬的都是我在交代 leo 該做什麼: 「下一步還是那一個動作:你把 feat/... 併進 main」 「## 你下一步(兩招,先便宜的)」 ⇒ 這是全新的第四類誤攔,我原本一向都沒列到。 修法是主詞檢查(誰要動手),只掛在「下一步」這條 alternative 上; 用 finditer 逐個檢查前 8 字有沒有第二人稱,蓋得到「你的下一步」這種 單字 lookbehind 蓋不到的變體。「你點頭我就做」主詞本來就是我,不受影響。 ② 票號從「宣告句附近」放寬成「整段」,把閘變鈍了。 真數據:26 個真實回合有 20 個是靠「文中某處剛好有票號」放行的—— 而報告幾乎一定會提到票號 ⇒ 這道閘在實務上等於不會響。 當初放寬是因為票號常寫在行內 code 裡,剝掉就找不到。 ⇒ 改成**等長**替換(蓋成同樣長度的哨兵而非刪除),位移就能對回原文, locality 與「行內 code 裡的票號也算數」兩件同時成立。 收緊後的判定分佈:21 no-declaration / 3 dispatched / 2 ticket-referenced (原本是 20 ticket-referenced / 3 no-declaration / 3 dispatched)。 真 transcript 實測:舊閘擋 2 次(兩次都是誤攔)→ 新閘擋 0 次。 測試 33 向(+6):舊版 21/33 → 新版 33/33。 版號 0.3.7 → 0.3.8。 Co-Authored-By: Claude Opus 5 --- .claude-plugin/plugin.json | 2 +- hooks/factory-idle-guard.sh | 60 ++++++++++++++++++-------- hooks/tests/factory-idle-guard.test.sh | 26 +++++++++++ 3 files changed, 69 insertions(+), 19 deletions(-) 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 群:訊息承諾的每一條出路,都要真的走得通 ─────────────────"