feat(W2 Phase 0-1): 雙 profile 地基+JDD 兩軸身分+兩支防炸閘

SDD: docs/3-specs/jdd-dual-profile(draft → active,leo 2026-08-05 回「開工」)
範圍:總管指定的「防炸兩件 → Phase 0 → Phase 1」,Phase 2 以後未開工。

■ 防炸(排在所有 task 之前,因為它們炸的是既有的東西)
- check-no-instance-names.sh + instance-names.txt:框架範本不得混入實例專名
  基線實測 22 行命中(非先前誤報的 20)→ 9 行無損泛化改寫、13 行檔級豁免記帳待 W3 搬走
  拒絕假性清理(把專名換成模糊詞=資訊消失、分層問題還在)
- check-legacy-paths.sh:已發佈腳本引用的 35 條遠端路徑只增不移
  舊實例跑的是舊腳本、路徑寫死;搬檔=整排 404 且不會有下一次更新來修它(1.16.0 前科)

■ Phase 0 地基
- template/manifest/{common,repo,orchestrator}.tsv:安裝清單單一真相源
  修好 install/update 兩份硬編清單的既有漂移——install 從不裝 wiki-first-search /
  subagent-wiki-guard / publish-lag-check / decisions-summary,但 update 會
  ⇒ 乾淨安裝反而拿不到 1.16/1.17/1.18 的招牌功能
- .claude/hooks/lib/role-lib.sh:scope×role 兩軸機械判定,零自陳
  身分矩陣六組實測全通過,含「成員 repo × orchestrator」不存在的格子擋下
- .sdt-framework-dev:框架開發標記(官方沒有 --framework-dev 這個參數,實查非記憶)

■ Phase 1 雙 profile
- profiles/{repo,orchestrator}/CLAUDE.md 兩部憲法
- install.sh:--profile + 自動偵測+寫檔前確認、manifest 驅動、
  CLAUDE.md 三段組裝(框架區/本地補充區界標+sha256)、.profile、.template-manifest、
  settings.json 寫入 env.AGENT_ROLE 預設
- update.sh:漂移偵測(不覆蓋手改檔、另存 .new、白話清單)+ 基準快照隨更新前進
- template/CLAUDE.md 原路徑凍結留底(相容)

■ 順手修掉兩個舊 bug(都在本次要動的函式裡)
- add_if_missing 少了 mkdir -p ⇒ 新目錄的檔 curl 失敗但 VERSION 照升(2026-07 記「待回報」至今未修)
- 下載健全性只用 [ -s ]=非空即接受 ⇒ 404 頁面會無聲覆寫好檔
  (SKILL.md 260→1 行的機制;同一支腳本的版本號那條路早就防了,檔案這條沒防)

■ 實測(非推論)
- G4 憲法分流:兩個乾淨環境各裝一次,orchestrator 版含 SDD 三件式關鍵字 0 次、
  repo 版含上游指針 8 次;界標 4/4;sha 宣告與實算相符
- G7 CI 擋實例名:注入違規行 → fail 並指出 sdd-check.md:77,exit 1;還原後 exit 0
- 漂移偵測:手改兩支 hook → 正確報 2 支、手改內容保住、產 .new;
  解掉後歸零;連跑三輪冪等

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 23:56:48 +08:00
parent 2a8c259d08
commit 6a49f25aef
23 changed files with 1259 additions and 132 deletions
+137 -4
View File
@@ -127,13 +127,88 @@ if [ "$NEEDS_MIGRATE" = "yes" ]; then
echo ""
fi
# ── 漂移偵測(1.19.0 新增)─────────────────────────
# 病根:實例會手改安裝產物(hook、範本),而**沒有任何機制知道**。
# 實測基線(2026-08-05,拿一個真實使用中的實例對照框架):
# 6 支框架 hook 裡 **4 支已被手改**,另有 11 支是該實例自己發明的。
# 兩層後果:① 框架修好的 bug 送不到那些檔 ② 手改內容沒人審、沒回饋回框架
# ⇒ 同一個坑每個實例各踩一次。
#
# 判準:安裝當下的 sha(記在 system-dev/.template-manifestvs 現在的 sha。
# 相同 → 沒動過 → 安全覆蓋成新版
# 不同 → **被手改過** → 不覆蓋、另存 .new、列進漂移清單,讓人自己決定
#
# 為什麼不直接覆蓋:無聲吃掉別人的修改(他多半是在修一個框架還沒修的問題)。
# 為什麼不永遠不覆蓋:那更新就永遠送不到 = 等於沒有更新機制。
# **分辨得出來**才是解法,兩個極端都不對。
DRIFTED=()
sdt_sha256() {
if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'
elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
else printf 'nohash'; fi
}
MANIFEST_FILE="system-dev/.template-manifest"
manifest_sha() { # 查登記的 sha;查不到回空
[ -f "$MANIFEST_FILE" ] || return 0
awk -F'\t' -v d="$1" '!/^#/ && $1==d {print $4; exit}' "$MANIFEST_FILE"
}
# 本次 update 經手過的檔(不論更新、保留或漂移)。收工時用它重寫 manifest。
# ⚠️ 沒有這一步的話會出現**自己造的 bug**:update 把檔覆蓋成新版後,manifest 還留著舊 sha
# ⇒ 下一次跑 update 會把「框架自己更新的檔」誤判成「使用者手改」,整排假漂移。
# 基準快照必須跟著實檔一起前進。
MANAGED=()
is_drifted() { # 0=被手改過;1=沒動過或無從判斷(保守,不誤報)
local dest="$1" recorded
recorded="$(manifest_sha "$dest")"
[ -z "$recorded" ] && return 1
[ -f "$dest" ] || return 1
[ "$(sdt_sha256 "$dest")" = "$recorded" ] && return 1
return 0
}
# ── 下載健全性檢查(修一個舊 bug)──────────────────
# 原本只用 [ -s ]=「非空就接受」。但 curl 對 404 會把「404: Not Found」當內容寫出來,
# **那也是非空** ⇒ 好檔被覆寫成一行垃圾,而且沒有任何錯誤訊息。
# 實錄:SKILL.md 曾被無聲覆寫 260 行 → 1 行(template issue #13)。
# 諷刺的是本腳本的「版本號比對」那段早就防了這招(見上方 REMOTE_VER 的驗證),
# 檔案下載這條路卻沒防——同一個坑,防了一半。
looks_like_error_page() {
local f="$1"
[ -s "$f" ] || return 0 # 空的 → 當失敗
# 極短又長得像錯誤訊息 → 當失敗(正常範本檔不會只有一兩行還寫著 404)
if [ "$(wc -l < "$f" | tr -d ' ')" -le 2 ] && head -c 200 "$f" | grep -qiE '40[0-9]|not found|<html'; then
return 0
fi
return 1
}
# ── 工具函式 ───────────────────────────────────────
# 覆蓋更新:模板/邏輯檔,無條件抓最新版蓋掉。
# 覆蓋更新:模板/邏輯檔,抓最新版蓋掉——**除非它被手改過**
update_file() {
local dest="$1" src="$2"
mkdir -p "$(dirname "$dest")"
MANAGED+=("$dest\toverwrite")
if [ -f "$dest" ]; then
if curl -sSL "$src" -o "$dest.tmp" 2>/dev/null && [ -s "$dest.tmp" ]; then
if is_drifted "$dest"; then
# 被手改過:不覆蓋,把新版放旁邊供 diff
if curl -sSL "$src" -o "$dest.new" 2>/dev/null && ! looks_like_error_page "$dest.new"; then
if cmp -s "$dest" "$dest.new"; then
rm -f "$dest.new" # 手改後剛好等於新版 → 沒事,不用吵
else
DRIFTED+=("$dest")
fi
else
rm -f "$dest.new"
DRIFTED+=("$dest")
fi
return 0
fi
if curl -sSL "$src" -o "$dest.tmp" 2>/dev/null && ! looks_like_error_page "$dest.tmp"; then
if cmp -s "$dest" "$dest.tmp"; then
rm -f "$dest.tmp" # 內容相同,不算更新
else
@@ -145,7 +220,7 @@ update_file() {
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
fi
else
if curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then
NEW+=("$dest") # 新功能:舊版沒有的檔
else
rm -f "$dest"
@@ -163,12 +238,19 @@ keep_file() {
# 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。
add_if_missing() {
local dest="$1" src="$2"
MANAGED+=("$dest\tadd-if-missing")
if [ -f "$dest" ]; then
KEPT+=("$dest")
elif curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
return 0
fi
# 🐛 修:原本少了 mkdir -p ⇒ 目標在**新目錄**時 curl 直接失敗,
# 但 VERSION 照樣升上去 = 典型假綠(2026-07 在一個實例升級時撞到,當時記「待回報 template」)。
mkdir -p "$(dirname "$dest")"
if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then
NEW+=("$dest")
else
rm -f "$dest"
t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest"
fi
}
@@ -318,6 +400,29 @@ if [ ${#UPDATED[@]} -gt 0 ]; then
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
fi
if [ ${#DRIFTED[@]} -gt 0 ]; then
echo ""
t "⚠️ 下列 ${#DRIFTED[@]} 個檔被手改過,這一版**沒有**覆蓋它們:" \
"⚠️ ${#DRIFTED[@]} file(s) were modified by hand and were NOT overwritten:"
for f in "${DRIFTED[@]}"; do
if [ -f "$f.new" ]; then
t "$f → 新版已放在 $f.new,請 diff" \
"$f → new version saved as $f.new — please diff"
else
t "$f (新版抓取失敗,稍後再試)" \
"$f (couldn't fetch the new version; try again later)"
fi
done
echo ""
t " 這代表什麼:你(或某個 CC)改過框架發下來的檔。" \
" What this means: you (or a CC) edited a file that the framework ships."
t " 兩條路,擇一——別放著不管,放著=框架之後的修正永遠送不到這個檔:" \
" Pick one — leaving it means future framework fixes will never reach this file:"
t " ① 把你的改動寫成框架提案(推薦)——一次修全家,別人也拿得到" \
" 1. Turn your change into a framework proposal (recommended) — fixes it for everyone"
t " ② 放棄本地改動:mv <檔>.new <檔>" \
" 2. Drop your local change: mv <file>.new <file>"
fi
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
echo ""
t "✨ 模板邏輯檔已全部最新,無需變動。" \
@@ -387,6 +492,34 @@ PYEOF
fi
fi
# ── 重寫漂移基準快照(manifest)──────────────────────
# 兩個作用:
# ① 舊實例(1.18.x 以前沒有 manifest)→ 這一輪之後就有了,下次起就能偵測漂移。
# 注意本輪它們**不會**被判漂移(查無登記=不判定),這是刻意的保守:
# 沒有基準就宣稱「你改過」等於瞎猜。第一輪建基準,第二輪起才有話語權。
# ② 已有 manifest → 把「本輪更新過的檔」的 sha 推進到新值。
# 少了這步就會自己造一個 bug:框架自己覆蓋的檔,下一輪被誤判成使用者手改。
# 漂移中的檔**不更新其基準**——它們仍然是「相對於安裝版被改過」,下次還要繼續報。
{
printf '# dest\tclass\tversion\tsha256 —— update 維護的漂移基準,勿手改\n'
for entry in ${MANAGED[@]+"${MANAGED[@]}"}; do
m_dest="$(printf '%b' "$entry" | cut -f1)"
m_class="$(printf '%b' "$entry" | cut -f2)"
[ -f "$m_dest" ] || continue
skip=""
for d in ${DRIFTED[@]+"${DRIFTED[@]}"}; do
[ "$d" = "$m_dest" ] && skip="yes" && break
done
if [ -n "$skip" ]; then
# 保留原登記值(它還在漂移中,基準不能跟著漂)
old="$(manifest_sha "$m_dest")"
[ -n "$old" ] && printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$LOCAL_VER" "$old"
continue
fi
printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$REMOTE_VER" "$(sdt_sha256 "$m_dest")"
done
} > "$MANIFEST_FILE.tmp" && mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
echo ""
t "🚀 更新完成:${LOCAL_VER}${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER}${REMOTE_VER}"
t " 下次更新直接跑:bash system-dev/scripts/update.sh" " Next time, just run: bash system-dev/scripts/update.sh"