feat(daemon-beta t2): template 代裝——embed v1.18.0 快照(36檔)/冪等不覆寫/daemon 啟動自動鋪/template-install 子命令;scan 加 SkipDirNames 防 system-dev 與 CLAUDE.md 被當知識掃
- 測試 3 支新增全綠(首鋪/冪等護用戶檔/掃描跳 template 產物——第三支先紅抓到 CLAUDE.md 洩入再修綠) - CLI 實跑:新鋪 36 檔→重跑 0 新檔(冪等證據)
This commit is contained in:
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse hook 範本骨架 —— 專案自訂禁令(預設空殼,不攔任何東西)
|
||||
#
|
||||
# ⚠️ 定位(讀清楚再用):
|
||||
# 這支跟其他三支 hook 不同——它不是「裝上就生效的警察」,而是一個「按需手填的
|
||||
# 空插槽」。預設狀態下 FORBIDDEN_PATTERNS 是空的,它【不攔任何東西】。
|
||||
# 別誤以為裝了它就有保護——空殼 = 沒保護。
|
||||
#
|
||||
# 🤖 有 CC 在場的話,通常不需要這個範本:
|
||||
# 直接叫你的 CC「幫我寫一支 guard hook,禁止改 X」。CC 現寫的條件邏輯,
|
||||
# 表達力遠勝這裡的 glob FORBIDDEN_PATTERNS(例如「禁子 repo 的 code 但放行 .md」
|
||||
# 這種細緻規則,glob 寫不出來,CC 的條件判斷寫得出來)。
|
||||
# 這個範本只對「不靠 CC、想自己手動 DIY bash」的用戶有價值。
|
||||
#
|
||||
# 要啟用(手動 DIY 路線):
|
||||
# 1. 在下面 FORBIDDEN_PATTERNS 填禁改的路徑/檔名 pattern
|
||||
# 2. 到 .claude/settings.json 的 PreToolUse 加掛這支
|
||||
#
|
||||
# 掛在 PreToolUse(matcher: Write|Edit)。stdin 收 JSON:{ tool_name, tool_input:{ file_path } }
|
||||
# 命中禁令 → exit 2 擋。
|
||||
#
|
||||
# 誠實限制:只擋直接寫檔。bash 繞道、helper 間接改動擋不到。留痕可審 ≠ 技術防偽。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── 專案自訂:禁改的 pattern(一行一個,case glob 語法)──────
|
||||
# 範例(已註解,啟用前請改成自己的):
|
||||
# "*/db/schema.sql" # 禁手改 schema
|
||||
# "*/migrations/*" # migration 一旦建立不可改
|
||||
FORBIDDEN_PATTERNS=(
|
||||
# "*/your/protected/path/*"
|
||||
)
|
||||
|
||||
# 沒設任何禁令 → 空殼狀態,安靜放行。
|
||||
# (不在這裡 print——PreToolUse 每次 Write/Edit 都會跑,每次喊話會洗版。
|
||||
# 「這是空殼」的提醒改由 install.sh / update.sh 安裝時告知,那裡用戶一定看得到。)
|
||||
[ ${#FORBIDDEN_PATTERNS[@]} -eq 0 ] && exit 0
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
else
|
||||
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||
fi
|
||||
|
||||
[ -z "$FILE_PATH" ] && exit 0
|
||||
|
||||
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
|
||||
# shellcheck disable=SC2254
|
||||
case "$FILE_PATH" in
|
||||
$pattern)
|
||||
cat >&2 <<EOF
|
||||
🚫 專案禁令攔截:$FILE_PATH 命中禁改規則($pattern)。
|
||||
|
||||
這是本專案 .claude/hooks/pre-write-guard.sh 設定的硬底線。
|
||||
要動 → 先和負責人確認,並更新禁令設定。
|
||||
EOF
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
exit 0
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# publish-lag-check.sh — SessionStart hook:偵測「公開 mirror 落後工作區」並出聲
|
||||
#
|
||||
# 病根(leo 2026-07-21 點名的真實風險):
|
||||
# Gitea(草稿/工作現場)與 GitHub(正稿/成品櫥窗)是**手動同步**的
|
||||
# (靠人跑 scripts/publish-github.sh --push,且需 D20 arm)。
|
||||
# → 改了零件、重編 wasm 後若沒人記得發佈,**用戶抓到舊版且沒有任何錯誤訊息,
|
||||
# 只是行為不對**——這種靜默失敗只有外部使用者會撞到,我們自己永遠測不到。
|
||||
#
|
||||
# 實例:安裝器的懶載會從
|
||||
# cdn.jsdelivr.net/gh/youlinhsieh/Arcrun@main/.component-builds/<名>/component.wasm
|
||||
# 抓 wasm。那個位址永遠指向 GitHub 上的**最後一次發佈**,不是我們本機的最新版。
|
||||
#
|
||||
# 原理:比對「工作區 HEAD」與「.github-public 最後一個 release commit 記錄的 snapshot」。
|
||||
# publish-github.sh 的 commit 訊息格式固定為:release: snapshot <短hash> (<日期>)
|
||||
# → 從中取出 hash,看它是不是工作區 HEAD 的祖先/相同。
|
||||
#
|
||||
# 只提醒不阻擋(exit 0):發不發佈是人的決定(且 push GitHub 需 leo 親跑 arm),
|
||||
# hook 的職責只是消滅「忘了」這個失敗模式。
|
||||
set -euo pipefail
|
||||
|
||||
MIRROR_DIR=".github-public"
|
||||
|
||||
# 沒裝發佈管線的 repo 直接安靜退出
|
||||
[ -d "$MIRROR_DIR/.git" ] || exit 0
|
||||
[ -f "scripts/publish-github.sh" ] || exit 0
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
||||
|
||||
HEAD_SHORT="$(git rev-parse --short HEAD 2>/dev/null || echo '')"
|
||||
[ -z "$HEAD_SHORT" ] && exit 0
|
||||
|
||||
# 從 mirror 最後一個 commit 訊息取出它當初發佈的來源 hash
|
||||
LAST_MSG="$(git -C "$MIRROR_DIR" log -1 --format=%s 2>/dev/null || echo '')"
|
||||
PUBLISHED="$(printf '%s' "$LAST_MSG" | sed -n 's/.*snapshot \([0-9a-f]\{6,\}\).*/\1/p')"
|
||||
|
||||
if [ -z "$PUBLISHED" ]; then
|
||||
# mirror 存在但沒有可辨識的 release commit(可能還沒發過)
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "📦 這個 repo 有公開發佈管線,但 mirror 還沒發過任何版本"
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo " 若已有用戶依賴公開版(例如安裝器從 jsDelivr 抓 wasm),現在是空的。"
|
||||
echo " 發佈:leo 在頂層跑 scripts/github-arm.sh,再於本 repo 跑"
|
||||
echo " GITHUB_REMOTE=... bash scripts/publish-github.sh --push"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 已發佈的那個 commit 就是現在的 HEAD → 同步,安靜
|
||||
if [ "$PUBLISHED" = "$HEAD_SHORT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 算出落後幾個 commit(發佈點 → HEAD)。取不到就不顯示數字。
|
||||
BEHIND="$(git rev-list --count "${PUBLISHED}..HEAD" 2>/dev/null || echo '')"
|
||||
|
||||
# 落後 0 且 hash 不同 → 可能是 mirror 比工作區新(罕見,例如剛 rebase),一樣提醒
|
||||
echo "════════════════════════════════════════════════"
|
||||
if [ -n "$BEHIND" ] && [ "$BEHIND" != "0" ]; then
|
||||
printf '📤 公開 mirror 落後工作區 %s 個 commit(最後發佈:%s,現在:%s)\n' \
|
||||
"$BEHIND" "$PUBLISHED" "$HEAD_SHORT"
|
||||
else
|
||||
printf '📤 公開 mirror 與工作區不一致(最後發佈:%s,現在:%s)\n' "$PUBLISHED" "$HEAD_SHORT"
|
||||
fi
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "⚠️ 外部使用者拿到的仍是舊版,而且**不會有任何錯誤訊息**——只是行為不對。"
|
||||
echo " (安裝器的懶載直接從公開位址抓 wasm,落後=裝到舊零件。)"
|
||||
echo ""
|
||||
echo " 要發佈:① leo 在頂層跑 bash scripts/github-arm.sh \"<任務描述>\" 30"
|
||||
echo " ② 本 repo 跑 GITHUB_REMOTE=https://github.com/<帳號>/<repo>.git \\"
|
||||
echo " bash scripts/publish-github.sh --push"
|
||||
echo " 不急著發也沒關係——這只是提醒,別讓它靜默漏掉。"
|
||||
|
||||
# 若這次落後的內容碰到 wasm,額外警告(那是用戶會直接抓的東西)
|
||||
if git diff --name-only "${PUBLISHED}..HEAD" 2>/dev/null | grep -q '\.wasm$'; then
|
||||
echo ""
|
||||
echo " 🔴 這批改動**包含 .wasm 變更** → 用戶抓到的零件會跟你本機不同,優先發佈。"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse hook — 動 code 前檢查 SDD + 單一活性 SDD 鐵律(issue #6)
|
||||
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
|
||||
# 生命週期規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md
|
||||
#
|
||||
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
||||
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, ... } }
|
||||
# 行為:
|
||||
# 1. status: active 的 SDD > 1 份 → 單一活性鐵律已被違反,**不論寫什麼檔**一律擋(exit 2),
|
||||
# 先收斂到一份再說。
|
||||
# 2. 動 code 檔(.ts/.go/...)→ 需要「恰好 1 份」active SDD;0 份 → 擋。
|
||||
# 3. 向下相容:3-specs 下完全沒有任何 design.md 帶 frontmatter(老 repo 尚未遷移生命週期制度)
|
||||
# → 退回舊行為:有 design.md 就放行+提醒,沒有才擋。避免 template update 後老 repo 立刻全紅。
|
||||
#
|
||||
# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。
|
||||
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
|
||||
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# 解析 file_path。優先用 jq,沒有 jq 退回 grep(容錯)。
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
else
|
||||
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||
fi
|
||||
|
||||
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
||||
[ -z "$FILE_PATH" ] && exit 0
|
||||
|
||||
SPECS_DIR="system-dev/docs/3-specs"
|
||||
|
||||
# ── 統計 active / frontmatter ──────────────────────
|
||||
# 排除 archive/(已封存)與 TEMPLATE(範本自帶 status: draft frontmatter,不算數——
|
||||
# 否則 update 一鋪新版 TEMPLATE-sdd,老 repo 就被誤判「已遷移」而全紅,向下相容破功)。
|
||||
# frontmatter 判定=design.md 前 10 行有 ^status: 行(機器可查,見 SDD-LIFECYCLE.md)。
|
||||
ACTIVE_COUNT=0
|
||||
FM_COUNT=0
|
||||
ACTIVE_LIST=""
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
HEAD10=$(head -10 "$f" 2>/dev/null || true)
|
||||
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*'; then
|
||||
FM_COUNT=$((FM_COUNT + 1))
|
||||
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*active'; then
|
||||
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
||||
ACTIVE_LIST="${ACTIVE_LIST} • ${f}
|
||||
"
|
||||
fi
|
||||
fi
|
||||
done < <(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── 鐵律 1:單一活性被違反(active > 1)→ 不論寫什麼檔一律擋 ──
|
||||
if [ "$ACTIVE_COUNT" -gt 1 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 單一活性鐵律違反:偵測到 ${ACTIVE_COUNT} 份 status: active 的 SDD(任何時刻整個 repo 最多一份):
|
||||
${ACTIVE_LIST}
|
||||
請先收斂到一份:其餘改 status: paused / closed(closed 且被取代者填 superseded_by 並移入 3-specs/archive/)。
|
||||
規則全文見 system-dev/docs/3-specs/SDD-LIFECYCLE.md。收斂前擋下所有寫檔。
|
||||
(本 hook 攔 Write/Edit;修 frontmatter 可用 bash 直改,或由人裁決哪份是現行。)
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 只管 code 檔。docs/markdown/設定檔等放行。
|
||||
case "$FILE_PATH" in
|
||||
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# 改 SDD 自己 / 測試檔 → 放行
|
||||
case "$FILE_PATH" in
|
||||
*system-dev/docs/3-specs/*) exit 0 ;;
|
||||
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# ── 向下相容:整個 3-specs 沒有任何帶 frontmatter 的 design.md ──
|
||||
# =老 repo 還沒遷移生命週期制度 → 退回舊行為(有 design.md 就放行+提醒),
|
||||
# 避免 template update 一裝新 hook,老 repo 所有 code 寫入立刻全紅。
|
||||
if [ "$FM_COUNT" -eq 0 ]; then
|
||||
SDD_COUNT=0
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
SDD_COUNT=$(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ "$SDD_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下找不到任何 SDD。
|
||||
|
||||
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md),且遵守單一活性生命週期
|
||||
(system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||
|
||||
請先:
|
||||
1. 確認這個改動屬於哪個子系統
|
||||
2. 在 ${SPECS_DIR}/[子系統]/ 建立 design.md(可用 /sdd-check 協助),frontmatter 標 status: active
|
||||
3. 在回覆開頭宣告已讀 SDD + 對應 task
|
||||
|
||||
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 舊行為放行 + 提醒遷移(stderr 警告,不擋)
|
||||
echo "📋 提醒:${SPECS_DIR}/ 有 SDD 但尚未掛生命週期 frontmatter(老結構)。動手前確認已讀對應 design.md;建議依 SDD-LIFECYCLE.md 補 status 標記(現行那份標 active)。" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 新行為:寫 code 檔需「恰好 1 份」active SDD ──
|
||||
if [ "$ACTIVE_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下沒有任何 status: active 的 SDD。
|
||||
|
||||
單一活性鐵律:所有開發任務唯一對應源=那份 active SDD(規則見 system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||
|
||||
請先(擇一,都是人的決定,CC 不得自行建 SDD):
|
||||
1. 把現行規格的 design.md frontmatter 標成 status: active(一份、只能一份)
|
||||
2. 或依 SDD-LIFECYCLE.md 第 3、4 條:proposal 進 pending-changes.md → 使用者 confirm → 開新 SDD 標 active
|
||||
然後在回覆開頭宣告已讀 active SDD + 對應 task。
|
||||
|
||||
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 恰好 1 份 active:放行,留痕提醒要宣告(stderr 警告,不擋)
|
||||
printf '📋 提醒:現行 active SDD=\n%s動手前請確認已讀它的 design.md、對應到 tasks,並在回覆宣告。\n' "$ACTIVE_LIST" >&2
|
||||
exit 0
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# SessionStart hook — 開 session 自動注入 status.md 重點
|
||||
# wishlist §1 主路徑:不靠 CC 自覺、不用人說,開 session 就把進度推到眼前。
|
||||
#
|
||||
# 掛在 settings.json 的 SessionStart(matcher: startup|resume|clear)。
|
||||
# stdout 會被當成 context 注入給 CC。
|
||||
#
|
||||
# 鐵律:status 是 point-in-time 快照,非即時狀態。
|
||||
# 這個 hook 只負責「把快照推到眼前」,核實快照是 CC 的責任——下面的提醒就是要它別盲信。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STATUS_FILE="system-dev/wiki/status.md"
|
||||
PRINCIPLES_FILE="system-dev/wiki/principles.md"
|
||||
MISTAKES_FILE="system-dev/wiki/mistakes.md"
|
||||
|
||||
# ── 舊結構防呆(1.9.0 遷移第 2 層保險)──
|
||||
# 新版 wiki 收進 system-dev/。若偵測到舊位置 .claude/wiki/ 還在、但新位置沒 status,
|
||||
# 代表使用者升級了規則卻沒跑遷移(low-code 使用者常見)→ 出聲提示,讓 CC 當場可代為遷移,
|
||||
# 不要默默用不到新結構而出錯。
|
||||
if [ -d ".claude/wiki" ] && [ ! -f "$STATUS_FILE" ]; then
|
||||
echo "⚠️ 偵測到舊版 wiki 結構(.claude/wiki/),尚未遷移到 system-dev/wiki/。"
|
||||
echo " 請跑:bash system-dev/scripts/update.sh"
|
||||
echo " 或直接叫我(CC):「幫我把 wiki 遷移到 system-dev/」——我可以代為搬移。"
|
||||
echo " (未遷移時接關與 /wiki-init 會找錯位置。)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 三個 push 檔都沒有 → 安靜退出,不干擾還沒 /wiki-init 的專案
|
||||
if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── push 1/3:principles(全文,行動前必服從)──
|
||||
# 放最前:原則是「會被遺忘的盲區」,要第一眼看見。全文成本低(一行一條、≤15 條)。
|
||||
if [ -f "$PRINCIPLES_FILE" ] && grep -q '^- ' "$PRINCIPLES_FILE" 2>/dev/null; then
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "📐 設計原則(行動前必服從,來自 principles.md)"
|
||||
echo "════════════════════════════════════════════════"
|
||||
grep '^- ' "$PRINCIPLES_FILE" # 只注入原則條目本身,不含說明區
|
||||
# context 保護:原則應 ≤15 條(push 全文)。超過 → 提示該合併或下放成 card,不截斷(截斷會漏原則)。
|
||||
P_COUNT=$(grep -c '^- ' "$PRINCIPLES_FILE")
|
||||
if [ "$P_COUNT" -gt 15 ]; then
|
||||
echo ""
|
||||
echo "(⚠️ principles 已 ${P_COUNT} 條 > 15:請考慮合併相近原則、或把較細的下放成 card)"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── push 2/3:status(全文,當前進度)──
|
||||
if [ -f "$STATUS_FILE" ]; then
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "📍 接關:上次進度(來自 $STATUS_FILE 快照)"
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo ""
|
||||
cat "$STATUS_FILE"
|
||||
fi
|
||||
|
||||
# ── push 3/3:mistakes(標題清單 + 一行症狀,全文按需展開)──
|
||||
# 不全文注入(可能累積很多、含長 context)。只給「標題行」讓 CC 認出「我正撞到某條」→ 再去讀全文。
|
||||
if [ -f "$MISTAKES_FILE" ] && grep -q 'MISTAKE' "$MISTAKES_FILE" 2>/dev/null; then
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────"
|
||||
echo "⚠️ 已知踩坑(標題;撞到時讀 $MISTAKES_FILE 全文)"
|
||||
grep -E 'MISTAKE' "$MISTAKES_FILE" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────"
|
||||
echo "⚠️ 以上是 point-in-time 快照,非即時狀態。"
|
||||
echo " 動手前先核實:快照寫的事項是否真的還沒做完?"
|
||||
echo " 需要完整脈絡(decisions / mistakes / SDD)→ 執行 /wiki-recall"
|
||||
echo "════════════════════════════════════════════════"
|
||||
|
||||
# ── 遷移殘留偵測(1.9.x):wiki 已搬到 system-dev/,但 CLAUDE.md 內容裡可能還寫舊路徑 ──
|
||||
# update.sh 鐵則「絕不碰 CLAUDE.md」(用戶資料),所以遷移不會自動改它內容裡的死引用。
|
||||
# 這裡只「偵測 + 提示 CC 代修」——由 CC(看得懂語義、不誤傷用戶自寫內容)來改,不讓腳本盲改。
|
||||
if [ -f "CLAUDE.md" ] && grep -q '\.claude/wiki' CLAUDE.md 2>/dev/null; then
|
||||
echo ""
|
||||
echo "🔧 偵測到 CLAUDE.md 內仍有舊路徑 \`.claude/wiki/\`(wiki 已遷移到 system-dev/wiki/)。"
|
||||
echo " 這些是遷移後的死引用,會讓你(CC)找錯位置。"
|
||||
echo " 請主動幫使用者把 CLAUDE.md 裡的 \`.claude/wiki/\` 改成 \`system-dev/wiki/\`、"
|
||||
echo " 工具 docs 路徑改成 \`system-dev/docs/\`(但 raw source 宣告的 \`docs/\` 保留不動)。"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
# subagent-wiki-guard.sh — PreToolUse(Task) hook:subagent 聽到「查」就自己先查 wiki
|
||||
#
|
||||
# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEY,prompt 都只叫它「去查 repo 程式碼」。
|
||||
# agent 於是從**稿子**推論出「這東西還活著、不能動」,總管照單全收去擋 leo 三輪。
|
||||
#
|
||||
# 🔑 設計轉向(leo 2026-07-21):
|
||||
# 第一版是「上游沒交代讀 wiki 就擋下」——但那**還是依賴上游記得寫**,
|
||||
# 跟「我記得讀 wiki」是同一個病。leo 點破:
|
||||
# 「subagent 的問題跟你一樣。你叫它去查,就算你沒說要先查 wiki,
|
||||
# 但它**只要聽到查,就應該主動查 wiki**,因為每個 repo 都有維護自己的 wiki。」
|
||||
# → 改成 **注入式**:不擋、不要求上游改 prompt,直接把「先查 wiki」這條
|
||||
# 以 additionalContext 注入給 subagent,讓它自己做。零依賴任何人記得。
|
||||
#
|
||||
# 行為:偵測到查證/實作類任務 → exit 0 並用 hookSpecificOutput 注入指示。
|
||||
# 已含 wiki 指示、或非查證類任務 → 靜默放行(不重複注入)。
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
PROMPT=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin)
|
||||
print(d.get('tool_input',{}).get('prompt',''))
|
||||
except Exception: print('')
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
[ -z "$PROMPT" ] && exit 0
|
||||
|
||||
# 上游已經交代了 → 不必重複注入
|
||||
if printf '%s' "$PROMPT" | grep -qiE "wiki|agent-memory|mistakes\.md|decisions-summary"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 只對「查證/實作」類任務注入(純寫作、計算、潤稿等不需要)
|
||||
if ! printf '%s' "$PROMPT" | grep -qiE "查|盤點|核實|確認|調查|研究|找出|repo|程式碼|原始碼|source|實作|移除|刪除|重構|修|grep|codebase|\.ts|\.go|src/"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
|
||||
guidance = """【自動注入:查任何東西之前,先查 wiki】
|
||||
|
||||
你所在的 repo 有維護自己的 wiki(通常在 `system-dev/wiki/`,舊結構在 `.claude/wiki/`)。
|
||||
**接到「查/盤點/核實/實作」類任務時,第一個動作是搜尋 wiki,不是翻程式碼。**
|
||||
|
||||
🔴 **查法有強弱之分,一律從最強的開始——沒有那個能力才降級。**
|
||||
(leo 2026-07-21:「它一定是用最好的搜尋,如果沒有才 fallback,
|
||||
但那不是你要指定的,對搜尋者來說,我就是要去搜尋,如果你沒這個機制才降。」)
|
||||
|
||||
**① 語意搜尋(最強,優先)**——有 Arcrun RAG MCP 就用它,用**自然語言問句**,不是關鍵字:
|
||||
kbdb_search(q="<用一句話描述你要找什麼>", mode="semantic")
|
||||
不確定該查哪個庫 → 先 kbdb_get_map() 看藏書地圖
|
||||
要沿關係展開 → kbdb_graph_neighbors()
|
||||
**② 關鍵字搜尋**——語意不可用時:kbdb_search(q="...", mode="keyword")
|
||||
**③ grep(最弱,最後手段)**——連 MCP 都沒有時:
|
||||
grep -rin "<關鍵字>" system-dev/wiki/ 2>/dev/null || grep -rin "<關鍵字>" .claude/wiki/
|
||||
|
||||
🔴 **為什麼順序是硬規定(2026-07-21 實際事故)**:
|
||||
查「CF 上的 git 託管」時只用了 grep,搜 Gitea/freeze/D43 等字面詞 → **零命中**,
|
||||
結論寫成「這件事沒查過、申請表沒送」。
|
||||
事後用**同一個問題**跑語意搜尋,**第一筆就命中**(score 0.858):
|
||||
「Cloudflare Artifacts:假設內建 git 倉庫機制的 CF 功能,成立則可全 CF 化」,
|
||||
還帶出三元組「Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea」——
|
||||
**負責人 15 天前就記在筆記裡了。**
|
||||
→ **grep 只認字面,要求你先猜對那個詞;語意搜尋不需要你猜對。**
|
||||
用 grep 查不到 ≠ wiki 沒記載,只代表你沒猜中用詞。
|
||||
|
||||
🔴 **凡結論涉及「某人沒做某事」,回報前必須先用語意搜尋查該事的記載**——
|
||||
這種結論錯了會變成**指控**,成本遠高於技術判斷錯誤。
|
||||
|
||||
為什麼這是划算的:
|
||||
• wiki 是前人已經查過、驗證過、被負責人糾正過的結論——**判準**。
|
||||
• 程式碼與歷史文件是**稿子**:它反映「還沒清乾淨」,不等於「還在用」。
|
||||
從稿子推論會系統性得出過時結論。
|
||||
• wiki 沒記載,才值得花力氣翻原文。
|
||||
• **凡結論涉及「某人沒做某事」,回報前必須先 grep 該事在 wiki 的記載**——
|
||||
這種結論錯了會變成指控,成本遠高於技術判斷錯誤。
|
||||
|
||||
三條硬規則:
|
||||
1. **wiki 與程式碼衝突 → 以 wiki 為準**,並在回報中明確指出衝突,
|
||||
不要自行用 code 推翻 wiki。
|
||||
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。
|
||||
那是當時狀態,不是永久禁令;條件已滿足就是可動。
|
||||
(2026-07-20 實際事故:agent 只看到「不可動」就回報不能動,
|
||||
實際上解除條件早已滿足,害負責人被擋三輪。)
|
||||
3. 翻原文後若得到**新結論**,回報時明講「wiki 該更新」——wiki 過時是債,要還。
|
||||
"""
|
||||
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"additionalContext": guidance
|
||||
}
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
|
||||
exit 0
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# wiki-first-search.sh — PreToolUse hook:要去翻原文/程式碼前,先把 wiki 命中結果推到眼前
|
||||
#
|
||||
# 病根(2026-07-20 leo 點破,mistakes 第一鐵律):
|
||||
# 總管 session 開頭讀了 agent-memory 前 50 行就開工,關鍵那條在第 56 行 → 拿過期記憶擋了 leo 三輪。
|
||||
# leo:「如果你不是讀而是**搜尋** wiki 就不會只讀 50 行就下定論,
|
||||
# 而是就像我直接在頁面 cmd+F,那些都會高亮。」
|
||||
#
|
||||
# 設計要點(為什麼是這個形狀):
|
||||
# 1. **搜尋 ≠ 通讀**:開場 push 全文(session-start-recall.sh)解決不了這題——量大必然只讀開頭。
|
||||
# 這支反過來:在「你正要去查 code/原文」的當下,用你自己的關鍵字 grep wiki,只推命中行。
|
||||
# 2. **時機是關鍵**:不是開場推、不是寫入時擋,而是**查詢動作發生的那一刻**介入。
|
||||
# 3. **提醒不阻擋**(exit 0):wiki 沒記載時本來就該去翻原文,擋下來反而礙事。
|
||||
# 唯一目的是消滅「不知道 wiki 有寫」這件事。
|
||||
#
|
||||
# 觸發:Grep / Glob / Read 打向 code 或 docs 時(見下方 should_check)。
|
||||
# 輸出:stdout 注入 context(命中的 wiki 行 + 檔名:行號)。
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL=$(printf '%s' "$INPUT" | python3 -c "import json,sys;print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "")
|
||||
|
||||
# 取出這次查詢的關鍵字:Grep 用 pattern,Glob/Read 用路徑的檔名部分
|
||||
QUERY=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys,os,re
|
||||
try:
|
||||
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||
q = ti.get('pattern') or ''
|
||||
if not q:
|
||||
p = ti.get('file_path') or ti.get('path') or ''
|
||||
q = os.path.splitext(os.path.basename(p))[0] if p else ''
|
||||
if not q:
|
||||
# Bash:2026-07-21 補的破口——原版只掛 Grep|Glob|Read,
|
||||
# 但「用 curl/wrangler 亂試部署方法」走的是 Bash,整支 hook 不觸發。
|
||||
# leo 當場點破:wiki 早記著「寄信已驗證可用」,我卻沒查又自創方法。
|
||||
# 只認「會動到外部系統/部署」的高風險指令,避免每個 ls 都洗版。
|
||||
cmd = ti.get('command') or ''
|
||||
if re.search(r'\b(wrangler|curl|npx|acr|gh|deploy|push)\b', cmd):
|
||||
# 取指令中最具識別度的詞(worker 名/資源名/子命令)當搜尋詞
|
||||
cand = re.findall(r'[A-Za-z_][A-Za-z0-9_-]{4,}', cmd)
|
||||
skip = {'https','http','client','accounts','workers','scripts',
|
||||
'application','content','Authorization','Bearer','python3',
|
||||
'curl','npx','bash','echo','grep','local','branch','origin'}
|
||||
cand = [c for c in cand if c not in skip and not c.startswith('-')]
|
||||
q = max(cand, key=len) if cand else ''
|
||||
# grep pattern 常含 regex 元字元;取最長的英數/底線詞當搜尋詞
|
||||
words = re.findall(r'[A-Za-z_][A-Za-z0-9_]{3,}', q)
|
||||
print(max(words, key=len) if words else '')
|
||||
except Exception:
|
||||
print('')
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
[ -z "$QUERY" ] && exit 0
|
||||
|
||||
WIKI_DIR="system-dev/wiki"
|
||||
[ -d "$WIKI_DIR" ] || exit 0
|
||||
|
||||
# 只在「查程式碼/文件」時提醒;查 wiki 本身就不用了(已經在讀了)
|
||||
TARGET=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||
print(ti.get('file_path') or ti.get('path') or '')
|
||||
except Exception: print('')
|
||||
" 2>/dev/null || echo "")
|
||||
case "$TARGET" in
|
||||
*system-dev/wiki*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# grep wiki(不分大小寫、含行號),最多 12 行避免洗版
|
||||
HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -12 || true)
|
||||
|
||||
# 🔴 grep 零命中時**不能靜默退出**——那正是今天失敗的模式(2026-07-21):
|
||||
# grep 查不到 → 以為 wiki 沒記載 → 結論「這件事沒查過」。
|
||||
# 但 grep 只認字面,查不到往往只代表「沒猜中用詞」。
|
||||
# → 零命中反而是**最該改用語意搜尋**的時刻,必須出聲。
|
||||
if [ -z "$HITS" ]; then
|
||||
echo "════════════════════════════════════════════════"
|
||||
printf '🔍 grep 在 wiki 找不到「%s」——但這**不代表沒記載**\n' "$QUERY"
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "grep 只認字面,查不到通常只是「沒猜中用詞」。**改用語意搜尋再確認一次**:"
|
||||
echo " kbdb_search(q=\"<用一句話描述你要找什麼>\", mode=\"semantic\")"
|
||||
echo " 不知道該查哪個庫 → kbdb_get_map()|要沿關係展開 → kbdb_graph_neighbors()"
|
||||
echo ""
|
||||
echo "實例:查「CF 上的 git 託管」時 grep 全零命中,語意搜尋第一筆就命中"
|
||||
echo "(Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea,負責人 15 天前就記了)。"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
COUNT=$(printf '%s\n' "$HITS" | wc -l | tr -d ' ')
|
||||
|
||||
echo "════════════════════════════════════════════════"
|
||||
printf '📚 wiki 已有「%s」的記載(%s 處,先看這裡再翻原文)\n' "$QUERY" "$COUNT"
|
||||
echo "════════════════════════════════════════════════"
|
||||
printf '%s\n' "$HITS" | sed 's|^system-dev/wiki/| |'
|
||||
echo ""
|
||||
echo "⚠️ wiki 是判準,程式碼與歷史文件只是稿子(mistakes 第一鐵律)。"
|
||||
echo " • 上面若與你將要查的原文衝突 → **以 wiki 為準**,別用 code 推翻 wiki。"
|
||||
echo " • 看到「不可動/待廢除/進行中」→ 先讀它的**解除條件**並逐條核對,"
|
||||
echo " 那是當時狀態不是永久禁令;條件已滿足就是可動。"
|
||||
echo " • wiki 沒答案才值得翻原文——翻完若得到新結論,**回頭更新 wiki**。"
|
||||
echo ""
|
||||
echo "🔎 以上是 **grep(最弱的查法)** 的結果,只認字面,且搜尋詞是從你的指令**猜**出來的"
|
||||
echo " (很可能太籠統而命中一堆無關的,同時漏掉真正的主題詞)。"
|
||||
echo " **重要判斷一律補一次語意搜尋**——它不需要你猜對用詞:"
|
||||
echo " kbdb_search(q=\"<一句話描述你要找什麼>\", mode=\"semantic\")"
|
||||
echo " 實例:查「CF 的 git 託管」時 grep 猜到的詞是 cloudflare → 命中 12 處全無關、"
|
||||
echo " 真正的答案(Artifacts)一筆沒撈到;語意搜尋第一筆就命中。"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse hook — 寫入 wiki 前掃機敏資訊(L3 硬攔截)
|
||||
#
|
||||
# 為什麼存在:wiki 的 ignore 規則(.wikiignore + 行內標記)是「協議層」,靠 CC 遵守。
|
||||
# 但密碼/金鑰/個資外洩是「不可逆」後果——只靠口頭約束太危險。
|
||||
# 這支 hook 是機械式底線:CC 真的把機敏資訊寫進 system-dev/wiki/ 的那一刻 → exit 2 擋下。
|
||||
#
|
||||
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
||||
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, content?, new_string? } }
|
||||
# 行為:只在目標路徑是 system-dev/wiki/** 時啟動,掃要寫入的內容,命中機敏特徵 → exit 2。
|
||||
#
|
||||
# 誠實限制(抄 sdd-guard):regex 偵測有偽陰/偽陽。
|
||||
# 擋的是「明顯特徵的機敏字串被自動抄進 wiki」,擋不了刻意混淆/編碼的繞道。
|
||||
# 價值是「意外外洩的機械底線 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# ── 解析 file_path 與要寫入的內容。優先 jq,無 jq 退回 grep(容錯)──────
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
# Write 用 content;Edit 用 new_string。兩個都抓,合起來掃。
|
||||
CONTENT=$(printf '%s' "$INPUT" | jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")')
|
||||
else
|
||||
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||
# 無 jq 時內容解析不可靠(JSON 跳脫),退回掃整包 INPUT,寧可多掃不漏掃
|
||||
CONTENT="$INPUT"
|
||||
fi
|
||||
|
||||
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
||||
[ -z "$FILE_PATH" ] && exit 0
|
||||
|
||||
# 只管寫進 wiki 的動作。其他路徑放行(這支專責 wiki 洩漏,不是全域 secret scanner)
|
||||
case "$FILE_PATH" in
|
||||
*system-dev/wiki/*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
[ -z "$CONTENT" ] && exit 0
|
||||
|
||||
# 行內豁免:若該段內容已被標記為刻意保留(例:範例文件要示範格式),略過該行
|
||||
# 標記:行尾加 # wiki-secret-ok (或 <!-- wiki-secret-ok -->)
|
||||
# 先把標記過的行抽掉再掃。
|
||||
SCAN=$(printf '%s' "$CONTENT" | grep -v -E 'wiki-secret-ok' || true)
|
||||
[ -z "$SCAN" ] && exit 0
|
||||
|
||||
# ── 機敏特徵 pattern。一行一類,命中即攔。──────────────────────────
|
||||
# 設計取捨:偏向高訊號 pattern(有明確結構的金鑰/標記),降低偽陽。
|
||||
# 純「password=xxx」這類也納入,因為那正是使用者最擔心的場景。
|
||||
HITS=""
|
||||
|
||||
check() {
|
||||
local label="$1" regex="$2"
|
||||
# -e 讓以 - 開頭的 pattern(如 PEM 的 -----BEGIN)不被當成選項。
|
||||
# grep 無命中回傳 1,在 set -e 下會中止 → 用 if 包住吸收掉。
|
||||
if printf '%s' "$SCAN" | grep -qiE -e "$regex"; then
|
||||
HITS="${HITS}
|
||||
• ${label}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 密碼/密鑰賦值(password = ..., secret: ..., api_key=...)
|
||||
check "密碼/密鑰賦值 (password/secret/api_key/token = ...)" \
|
||||
'(pass(word)?|secret|api[_-]?key|access[_-]?key|auth[_-]?token|priv(ate)?[_-]?key)[[:space:]]*[:=][[:space:]]*[^[:space:]<>"'"'"']{6,}'
|
||||
|
||||
# 私鑰 PEM 區塊
|
||||
check "私鑰檔內容 (BEGIN ... PRIVATE KEY)" \
|
||||
'-----BEGIN[[:space:]].*PRIVATE KEY-----'
|
||||
|
||||
# 常見雲端/服務金鑰前綴
|
||||
check "服務金鑰特徵 (AWS/GitHub/Slack/Google/Stripe 等)" \
|
||||
'(AKIA[0-9A-Z]{16}|gh[pousr]_[0-9A-Za-z]{20,}|xox[baprs]-[0-9A-Za-z-]{10,}|AIza[0-9A-Za-z_-]{20,}|sk_(live|test)_[0-9A-Za-z]{16,})'
|
||||
|
||||
# JWT
|
||||
check "JWT token" \
|
||||
'eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}'
|
||||
|
||||
# 連線字串內嵌帳密 (proto://user:pass@host)
|
||||
check "連線字串內嵌帳密 (proto://user:pass@host)" \
|
||||
'[a-z][a-z0-9+.-]*://[^[:space:]:/@]+:[^[:space:]:/@]+@'
|
||||
|
||||
# 台灣身分證字號(個資)。BSD/GNU grep 都支援 ERE,避免 \b(BSD 不認),改用字元類邊界。
|
||||
check "台灣身分證字號 (個資)" \
|
||||
'(^|[^A-Za-z0-9])[A-Z][12][0-9]{8}([^0-9]|$)'
|
||||
|
||||
# 信用卡號(個資,粗略 13-16 連續數字,可含空格/連字號分隔)。避免 PCRE,用 ERE 近似。
|
||||
check "疑似信用卡號 (個資)" \
|
||||
'(^|[^0-9])[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{0,4}([^0-9]|$)'
|
||||
|
||||
# Email 不擋(wiki 常需記聯絡人),手機號也不擋(偽陽太高)——刻意留白。
|
||||
|
||||
if [ -n "$HITS" ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 Wiki 機敏攔截:偵測到可能的機敏資訊要寫進 ${FILE_PATH}。
|
||||
|
||||
命中特徵:${HITS}
|
||||
|
||||
wiki 是會被 CC 反覆讀取、可能進版控的記憶空間。
|
||||
密碼 / 金鑰 / 個資寫進去 = 不可逆外洩風險。
|
||||
|
||||
請改成下列任一做法:
|
||||
1. 不要把機敏值寫進 wiki,改記「位置」(例:「DB 密碼放 1Password / .env,不入 wiki」)
|
||||
2. 確定是誤判(例:在示範格式)→ 該行尾加註記 wiki-secret-ok 後重寫
|
||||
3. 整個來源檔本就機敏 → 加進 system-dev/wiki/.wikiignore,別讓它被編入
|
||||
|
||||
誠實限制:本掃描靠特徵比對,有偽陽/偽陰,是「意外外洩的機械底線」而非保險箱。
|
||||
真正的密鑰本就不該進版控。
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user