5ac06abc95
三張票同一族(誰在派、派給誰、派的內容住哪裡),做在同一條分支: #86 工人名單:agents/ 七位有名字的工人+scripts/roster+hooks/roster-guard.sh 派工用 Task 的 subagent_type 指名,派工單格式一個字都沒改; 指對名字就把那位的檔案原文注入(你是誰/先讀什麼/你的紅線)。 【身份】欄同時吃得下工人名字(原本三個角色照舊)。 #87 未經調查不寫診斷:hooks/diagnosis-evidence-guard.sh + investigate-first-stamp.sh 三個結構訊號(派過人查沒/有沒有走得過去的出處/有沒有份量), 一個關鍵字比對都沒有;轉述有出處不會被誤擋。 #88 回覆也是派工:不另造閘,把攔截點加掛上去。 hooks/lib/dispatch_parse.py 的 tool_channel() 一次列全所有通往 subagent 的路 (SendMessage/雲端 session・trigger/claude -p);擋下來時把那段內容原文印出來。 subagent 往上回報(to: "main")=交件不是派工,刻意不管。 順手修掉一個真的會咬人的 flake:dispatch-format-guard 原本開四支 python 各讀一個欄位, 機器忙的時候某個欄位會靜靜變空字串(實測連跑 10 次有 1 次「豁免了卻還是被擋」)。 四個欄位改成一次讀完。 版本號待總管定(plugin.json 只更新了描述裡的數字,版本沒動)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZBu4Sa1cGntKFRBYNZ6xs
191 lines
10 KiB
Bash
Executable File
191 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 管什麼: 要把一段有份量的東西寫上票,而這個 session **沒派人查過那張票**、
|
||
# 那段話裡也**沒有任何走得過去的出處** → 擋一次。
|
||
# 為什麼: 未經調查的診斷寫在票上就長得像事實,工人會照著它去驗證而不是去查——
|
||
# 等於用權威把那個 repo 真正的專家的判斷力關掉。
|
||
# 誤觸時怎麼關: 把出處補上(檔案:行號/commit/comment 號/票號/網址/貼實測輸出),
|
||
# 或指令裡加 `evidence-ok`(留痕)。同一 session 同一張票只擋一次。
|
||
#
|
||
# diagnosis-evidence-guard.sh — 未經調查不寫診斷(PreToolUse: Bash;inkstone/ISEP#87)
|
||
#
|
||
# ━━ 來由(leo,inkstone/ISEP#87)━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
# 「身為派人去查的人,我要自己在拿到調查結果前寫不出診斷,
|
||
# 我才不會用猜的結論把工人的判斷力關掉。」
|
||
# 「**票裡寫未經調查的診斷**」是 2026-08-27 當天列出的錯之一。
|
||
#
|
||
# ⇒ 順序倒過來:**先派人查 → 拿到查的結果 → 才可以寫診斷。**
|
||
# 我的診斷只能建立在別人查回來的東西上面,不能建立在我的印象上面。
|
||
#
|
||
# ━━ 這支跟既有那兩支的分界(三支不重疊)━━━━━━━━━━━━━━━━━━━━━
|
||
# `subagent-first-guard.sh` 鐵律 2 的**前半**:要改 code 卻一次工都沒派過
|
||
# `micromanage-guard.sh` 替對方決定**做法**(給指令照跑、指名檔案函式…)
|
||
# **本支** 替對方決定**答案**(把還沒查證的結論寫進票)
|
||
# 前兩支都不會在「寫東西上票」這個動作上開口,所以不會兩支同時開罵。
|
||
#
|
||
# ━━ 🔴 判準:三個結構訊號,一個關鍵字都沒有 ━━━━━━━━━━━━━━━━━━━
|
||
# 票上寫死:「『根因』『因為』『應該是』這類詞**一律不准當判準**」
|
||
# (leo 2026-08-17 已證偽文字層封路:當日 8 次誤攔、0 次正確攔截,
|
||
# 而且**紅線寫得越細,命中關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎**)。
|
||
#
|
||
# 三個訊號,全部是結構:
|
||
# ① **這個 session 有沒有派人查過那張票**
|
||
# —— `investigate-first-stamp.sh` 的戳記(在不在,不是寫什麼)
|
||
# ② **這段話裡有沒有走得過去的出處**
|
||
# —— 檔案:行號/commit/comment 號/票號/網址/檔案路徑/貼出來的實測輸出。
|
||
# 方向是**放行**(認出來就放行),與黑名單相反:
|
||
# 比對錯了只會**少擋一次**,不會多罰一次(同 pr-verdict-guard ③)。
|
||
# ③ **這段話有沒有份量**(扣掉身份欄與空白後的字數,門檻 60 字,
|
||
# `ISEP_DIAGNOSIS_FLOOR` 可調)
|
||
# —— 一句「收到,我來看」(6 字)不可能是診斷。長度是結構,不是措辭。
|
||
# 門檻怎麼定的:拿一段真的診斷(「根因是 KV 額度爆掉⋯⋯」)實量 **73 字**,
|
||
# 拿最短的合理回覆實量 **6 字**——60 落在兩者之間,靠短的那邊。
|
||
# **不是猜的,是量過的**(第一版寫 120,那段真的診斷就漏抓了)。
|
||
#
|
||
# 三個訊號的組合:①有 ⇒ 放行(查過了)。①無 + ②有 ⇒ 放行(轉述有出處,
|
||
# 票上驗收條件 4 指明不能誤擋)。①無 + ②無 + ③夠長 ⇒ 擋一次。
|
||
#
|
||
# ━━ 誠實邊界(做不到的事就寫出來)━━━━━━━━━━━━━━━━━━━━━━━━
|
||
# · PreToolUse 只看得到指令文字與 `-F` 指到的檔(同 comment-carries-task-guard)。
|
||
# · 本閘**認不出「這段話是不是診斷」**——沒有任何機器認得出來,
|
||
# 而想認出來的那條路(句型比對)已經被證偽。它認的是
|
||
# 「**一段有份量、卻沒有任何出處、也沒人去查過**」的東西。
|
||
# 漏抓的情況:診斷寫得很短、或診斷裡剛好夾著一個無關的網址。
|
||
# 這是刻意的取捨——**誤攔比漏擋嚴重**,一支天天誤擋的閘會被學會忽略。
|
||
#
|
||
# 逃生口:指令裡加 `evidence-ok`(會留在指令歷史上)。同 session 同票只擋一次。
|
||
#
|
||
# 測試:hooks/tests/diagnosis-evidence-guard.test.sh(離線、不打網路、不留測試票)
|
||
set -uo pipefail
|
||
|
||
INPUT=$(cat)
|
||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
||
|
||
VERDICT=$(printf '%s' "$INPUT" | DEG_LIB="$HERE/lib" python3 -c '
|
||
import json, os, re, sys
|
||
sys.path.insert(0, os.environ.get("DEG_LIB", ""))
|
||
try:
|
||
import evidence
|
||
except Exception:
|
||
print("SKIP:no-lib"); raise SystemExit
|
||
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
except Exception:
|
||
print("SKIP:bad-payload"); raise SystemExit
|
||
|
||
cmd = (d.get("tool_input") or {}).get("command") or ""
|
||
if not cmd:
|
||
print("SKIP:no-cmd"); raise SystemExit
|
||
if "evidence-ok" in cmd: # 逃生口(留痕)
|
||
print("SKIP:escape"); raise SystemExit
|
||
|
||
sid = re.sub(r"[^A-Za-z0-9_.-]", "_", str(d.get("session_id") or "nosid"))[:64]
|
||
|
||
# ── ① 這是不是「把東西寫上票」的動作 ────────────────────────────────
|
||
# 兩條路都要認(`ticket-api-bypass-guard.sh` 檔頭記過那一課:
|
||
# 「認動作的方式漏了一條路」是這一族的通病)。
|
||
target = None # (owner, repo, num) 或 ("", repo_or_?, "new")
|
||
m = re.search(r"scripts/ticket\s+(\w+)", cmd) or re.search(r"\bticket\s+(say|new|decide|subtask)\b", cmd)
|
||
verb = m.group(1) if m else ""
|
||
if verb in ("say", "decide", "subtask"):
|
||
r = re.search(r"([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#(\d+)", cmd)
|
||
target = (r.group(1), r.group(2), r.group(3)) if r else ("", "", "?")
|
||
elif verb == "new":
|
||
r = re.search(r"ticket\s+new\s+([\w.-]+)", cmd)
|
||
target = ("inkstone", r.group(1) if r else "?", "new")
|
||
else:
|
||
is_post = re.search(r"\bPOST\b", cmd) or re.search(r"requests\.post|urlopen|\.post\(", cmd)
|
||
c = re.search(r"/repos/([\w.-]+)/([\w.-]+)/issues/(\d+)/comments", cmd)
|
||
n = re.search(r"/repos/([\w.-]+)/([\w.-]+)/issues\b", cmd)
|
||
if is_post and c:
|
||
target = (c.group(1), c.group(2), c.group(3))
|
||
elif is_post and n:
|
||
target = (n.group(1), n.group(2), "new")
|
||
|
||
if not target:
|
||
print("SKIP:not-a-ticket-write"); raise SystemExit
|
||
|
||
owner, repo, num = target
|
||
|
||
# ── ② 這個 session 派過人去查那張票了嗎(戳記在不在)──────────────────
|
||
stamp_dir = os.environ.get("ISEP_STAMP_DIR", "/tmp")
|
||
if num not in ("new", "?"):
|
||
if os.path.exists(os.path.join(stamp_dir, ".investigated-%s-%s_%s_%s"
|
||
% (sid, owner, repo, num))):
|
||
print("SKIP:investigated"); raise SystemExit
|
||
|
||
body, src = evidence.body_of(cmd)
|
||
|
||
# ── ③ 有出處嗎(放行方向的比對)──────────────────────────────────
|
||
hits = evidence.sources(body)
|
||
if hits:
|
||
print("SKIP:has-source:%s" % hits[0][0]); raise SystemExit
|
||
|
||
# ── ④ 有份量嗎(長度是結構,不是措辭)────────────────────────────
|
||
n_chars = evidence.substance(body)
|
||
try:
|
||
floor = int(os.environ.get("ISEP_DIAGNOSIS_FLOOR", "60"))
|
||
except Exception:
|
||
floor = 60
|
||
if n_chars < floor:
|
||
print("SKIP:too-short:%d" % n_chars); raise SystemExit
|
||
|
||
print("FIRE::" + json.dumps({"owner": owner, "repo": repo, "num": num,
|
||
"chars": n_chars, "src": src, "sid": sid},
|
||
ensure_ascii=False))
|
||
' 2>/dev/null) || VERDICT="SKIP:hook-error"
|
||
[ -n "${VERDICT:-}" ] || VERDICT="SKIP:empty"
|
||
|
||
case "$VERDICT" in
|
||
FIRE::*) ;;
|
||
*) exit 0 ;;
|
||
esac
|
||
|
||
REF=$(printf '%s' "${VERDICT#FIRE::}" | python3 -c '
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
print("%s/%s#%s" % (d["owner"], d["repo"], d["num"]) if d["num"] not in ("new", "?")
|
||
else ("新的一張票(%s)" % d["repo"]))
|
||
' 2>/dev/null)
|
||
SID=$(printf '%s' "${VERDICT#FIRE::}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["sid"])' 2>/dev/null)
|
||
KEY=$(printf '%s' "$REF" | tr -c 'A-Za-z0-9' '_')
|
||
WARNED="${ISEP_STAMP_DIR:-/tmp}/.diagnosis-guard-warned-$SID$KEY"
|
||
[ -f "$WARNED" ] && exit 0 # 同一 session 同一張票只擋一次,不鬼打牆
|
||
date +%s > "$WARNED" 2>/dev/null || true
|
||
|
||
DEG_REF="$REF" DEG_SID="$SID" python3 - <<'PY' >&2
|
||
import os
|
||
print("""🔎 這段要寫上 %s 的東西,**沒有人查過,也沒有任何出處**。
|
||
|
||
【inkstone/ISEP#87】「身為派人去查的人,我要自己在**拿到調查結果前寫不出診斷**,
|
||
我才不會用猜的結論把工人的判斷力關掉。」
|
||
|
||
· 診斷寫完了,**工人就不知道自己要做什麼**——它會照著你的結論去驗證,而不是去查
|
||
· **你不是那個 repo 的專家,它才是**。你的結論通常是猜的,
|
||
但因為寫在票上,看起來就像事實
|
||
|
||
━━ 為什麼是現在擋你(三個結構訊號,一個關鍵字都沒有)━━━━━━━━━━━━
|
||
① 這個 session **沒有派人去查這張票**(`investigate-first-stamp` 沒有戳記)
|
||
② 這段話裡**找不到任何走得過去的出處**
|
||
③ 而它**有份量**(不是一句「收到」)
|
||
|
||
━━ 兩條出路,選一條 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
A. **先派人去查**(順序倒過來,這是本票要的)
|
||
派工單就一行:【工單】%s
|
||
它查完貼回票上,你再寫診斷——那時①就成立了。
|
||
|
||
B. **這段本來就是轉述別人查到的,那就把出處帶上**(帶了就不會被擋)
|
||
檔案:行號 hooks/lib/evidence.py:42
|
||
commit 8e28041
|
||
comment 號 inkstone/ISEP#87 → comment 4327
|
||
網址/票號 https://git.uncle6.me/...
|
||
實測輸出 用 ``` 圍欄把指令與輸出原樣貼上
|
||
|
||
🔴 出處不是格式要求,是**讀的人可以自己走過去確認**。
|
||
「我記得是這樣」與「它就在那一行」的差別,只有這個分得出來。
|
||
|
||
真的有例外 → 指令裡加 `evidence-ok` 重送(會留在指令歷史上)。
|
||
同一張票本 session 只擋這一次。""" % (os.environ.get("DEG_REF", ""), os.environ.get("DEG_REF", "")))
|
||
PY
|
||
exit 2
|