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:
+137
-4
@@ -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-manifest)vs 現在的 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"
|
||||
|
||||
Reference in New Issue
Block a user