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
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# check-legacy-paths.sh — 相容閘:已發佈的安裝/更新腳本會抓的每個遠端路徑,都必須還在
#
# 這支存在的理由(SDD jdd-dual-profile 發現⑦ 決策 D3):
# install.sh / update.sh 是**逐檔 curl**:檔案清單與遠端路徑**寫死在腳本裡**。
# 而 update.sh 的「自我更新」在腳本**尾端**——舊實例這一輪跑的是**舊腳本**。
# ⇒ 只要框架把某個既有檔搬走/改名,舊實例的那一輪就是**整排 404**,
# 而且它們不會自動好:更新機制本身壞了,就沒有下一次更新來修它。
#
# 前科:1.16.0「update/install 來源改指 Gitea」——來源一改,
# 舊實例的自動更新當場死掉(CHANGELOG 1.16.0 有記)。同一種病。
#
# 所以鐵律是:**遠端路徑對既有實例是契約,只增不移。**
# 要搬版面 → 先在原路徑留相容檔(轉址說明/原內容),確認本閘綠了才准動。
#
# 作法:
# 從「已發佈版本」的 install.sh / update.sh 裡抽出所有 $REPO_URL/... $TEMPLATE_URL/...
# $SCRIPTS_URL/... 引用,映射回本 repo 的實體路徑,逐一確認檔案存在。
#
# 誠實限制:
# · 只驗「路徑還在不在」,不驗內容還對不對(舊腳本抓到新內容仍可能語意不相容)。
# · 基準是 git 裡的已發佈版本(預設 HEAD);再更老的版本若引用過更多路徑,
# 本閘看不到——要驗更早的版本,用 BASE=<commit> 指定。
#
# 用法:
# bash scripts/check-legacy-paths.sh # 以 HEAD 為已發佈基準
# BASE=dc4fe67 bash scripts/check-legacy-paths.sh
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
BASE="${BASE:-HEAD}"
# ── 取出基準版本的兩支腳本 ────────────────────────────
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
for s in install.sh update.sh; do
if ! git show "$BASE:scripts/$s" > "$TMP/$s" 2>/dev/null; then
echo "⚠️ 基準 $BASE 沒有 scripts/$s,略過" >&2
: > "$TMP/$s"
fi
done
# ── 抽出遠端引用 → 映射成本地實體路徑 ──────────────────
# $REPO_URL/x → template/x
# $TEMPLATE_URL/x → template/x
# $SCRIPTS_URL/x → scripts/x
extract_paths() {
grep -oE '\$\{?(REPO_URL|TEMPLATE_URL|SCRIPTS_URL)\}?/[A-Za-z0-9._/-]+' "$1" 2>/dev/null \
| sed -e 's|\${*REPO_URL}*/|template/|' \
-e 's|\${*TEMPLATE_URL}*/|template/|' \
-e 's|\${*SCRIPTS_URL}*/|scripts/|' \
|| true
}
ALL="$( { extract_paths "$TMP/install.sh"; extract_paths "$TMP/update.sh"; } | sort -u )"
if [ -z "$ALL" ]; then
echo "⚠️ 在 $BASE 的腳本裡找不到任何遠端引用——抽取規則可能過時,本閘等同關閉" >&2
exit 1
fi
TOTAL=0
MISSING=0
MISSING_LIST=""
while IFS= read -r p; do
[ -z "$p" ] && continue
TOTAL=$((TOTAL + 1))
if [ ! -e "$p" ]; then
MISSING=$((MISSING + 1))
MISSING_LIST="${MISSING_LIST}${p}"$'\n'
fi
done <<< "$ALL"
if [ "$MISSING" -gt 0 ]; then
cat >&2 <<EOF
════════════════════════════════════════════════
❌ 相容閘:已發佈腳本會抓的路徑不見了($MISSING / $TOTAL
════════════════════════════════════════════════
$MISSING_LIST
後果(不是理論,是 1.16.0 真的發生過):
舊實例跑 update 時對上面每個路徑 curl 404 → 更新失敗 →
**更新機制本身壞掉,之後也不會有下一次更新來修它**。
正解:搬版面時在原路徑留相容檔(轉址說明或原內容),
本閘綠了才准繼續。遠端路徑是契約,只增不移。
EOF
exit 1
fi
echo "✅ 相容閘:已發佈腳本(基準 $BASE)引用的 $TOTAL 個路徑全部還在"
exit 0
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# check-no-instance-names.sh — 機械閘 #2:框架範本不得混入實例專名
#
# 出處:《分離導入規格》第六節第 2 條、第七節第四題 Gherkin。
# Scenario: 框架混入實例名被 CI 擋
# Given 範本檔新增一行含 "arcrun"
# When CI 執行
# Then 檢查 fail 並指出檔案與行號
#
# 為什麼需要它(不是潔癖,是分層會自己塌):
# L1 框架的判準是「換一家公司照樣成立」。一旦範本裡寫著某個實例的專案名,
# 下一個裝這套 template 的人就會讀到一段對他不成立的規則——而他無從分辨
# 「這條是普世方法論」還是「這是別人家的家規」。分層一旦糊掉就回不去了。
#
# ── 豁免機制(為什麼要有)─────────────────────────────
# 本閘上線時,範本區已經有 22 行命中(8 個檔)。若不給豁免就硬上線,唯一的
# 通關方式會是「把 arcrun 改寫成『某工作流引擎』」——**資訊消失了,問題還在**
# (假性清理)。所以:真正該搬走的東西標豁免、記在帳上,隨 policy pack 一起搬;
# 純粹是註解舉例的,當場改寫成通用敘述。
# 豁免有兩級:
# · 行級 `sdt-instance-name-ok` → 該行豁免。用在「檔案本身是框架的,只有這行欠著」。
# · 檔級 `sdt-instance-name-ok-file` → 整檔豁免(寫在檔首 30 行內,需附理由)。
# 用在「整支本來就是 L2 政策包產物、只是還沒搬走」——逐行標 13 次是雜訊不是紀律。
#
# 誠實限制:這支只認「字面出現」。它擋不了「把實例假設寫成通用語氣」
# (例如寫「你的工作流引擎會在遠端執行」其實只有某一家成立)。
# 它是底線不是萬能——真正的分層判斷仍要人/CC 動腦。
#
# 用法:
# bash scripts/check-no-instance-names.sh # CI 模式,有未豁免命中 → exit 1
# bash scripts/check-no-instance-names.sh --baseline # 只列清單(含已豁免),永遠 exit 0
#
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
NAMES_FILE="scripts/instance-names.txt"
SCAN_DIR="template"
EXEMPT_MARK="sdt-instance-name-ok"
EXEMPT_FILE_MARK="sdt-instance-name-ok-file"
EXEMPT_HEAD_LINES=30 # 檔級豁免標記必須寫在檔首這麼多行內(強迫它顯眼、不許藏在檔尾)
MODE="ci"
[ "${1:-}" = "--baseline" ] && MODE="baseline"
if [ ! -f "$NAMES_FILE" ]; then
echo "❌ 找不到黑名單設定檔:$NAMES_FILE" >&2
exit 1
fi
if [ ! -d "$SCAN_DIR" ]; then
echo "❌ 找不到範本區:$SCAN_DIR/" >&2
exit 1
fi
# ── 讀黑名單 → 組成 grep 的 alternation pattern ───────────────
PATTERN=""
while IFS= read -r line; do
case "$line" in ''|'#'*) continue ;; esac
term="$(printf '%s' "$line" | tr -d '[:space:]')"
[ -z "$term" ] && continue
if [ -z "$PATTERN" ]; then PATTERN="$term"; else PATTERN="$PATTERN|$term"; fi
done < "$NAMES_FILE"
if [ -z "$PATTERN" ]; then
echo "⚠️ 黑名單是空的,本閘等同關閉($NAMES_FILE"
exit 0
fi
# ── 掃描 ────────────────────────────────────────────
# policy pack 不受此限(L2 本來就是一家之言)。目前尚無 policy pack 目錄,
# 先把慣例路徑寫在這裡,W3 建立時自動生效。
HITS="$(grep -rniE "$PATTERN" "$SCAN_DIR" 2>/dev/null \
| grep -v "^$SCAN_DIR/policy-packs/" || true)"
VIOLATIONS=0
EXEMPTED=0
VIOL_LINES=""
EXEMPT_LINES=""
if [ -n "$HITS" ]; then
while IFS= read -r hit; do
[ -z "$hit" ] && continue
hit_file="${hit%%:*}"
# 檔級豁免:整支本來就是 L2 產物,檔首宣告過就整檔記帳
if [ -f "$hit_file" ] && head -n "$EXEMPT_HEAD_LINES" "$hit_file" 2>/dev/null | grep -q "$EXEMPT_FILE_MARK"; then
EXEMPTED=$((EXEMPTED + 1))
EXEMPT_LINES="${EXEMPT_LINES}${hit}"$'\n'
continue
fi
# 行級豁免:檔案是框架的,只有這行欠著
if printf '%s' "$hit" | grep -q "$EXEMPT_MARK"; then
EXEMPTED=$((EXEMPTED + 1))
EXEMPT_LINES="${EXEMPT_LINES}${hit}"$'\n'
else
VIOLATIONS=$((VIOLATIONS + 1))
VIOL_LINES="${VIOL_LINES}${hit}"$'\n'
fi
done <<< "$HITS"
fi
# ── 輸出 ────────────────────────────────────────────
if [ "$MODE" = "baseline" ]; then
echo "════════════════════════════════════════════════"
echo "📊 實例專名基線報表($SCAN_DIR/"
echo "════════════════════════════════════════════════"
echo "未豁免(違規):$VIOLATIONS"
[ -n "$VIOL_LINES" ] && printf '%s' "$VIOL_LINES" | cut -c1-160 | sed 's/^/ ❌ /'
echo "已豁免(記在帳上,待搬遷):$EXEMPTED"
[ -n "$EXEMPT_LINES" ] && printf '%s' "$EXEMPT_LINES" | cut -c1-160 | sed 's/^/ ⏳ /'
exit 0
fi
if [ "$VIOLATIONS" -gt 0 ]; then
echo "════════════════════════════════════════════════" >&2
echo "❌ 框架範本混入實例專名($VIOLATIONS 行)" >&2
echo "════════════════════════════════════════════════" >&2
printf '%s' "$VIOL_LINES" | cut -c1-200 | sed 's/^/ /' >&2
cat >&2 <<EOF
鐵律:框架不含實例資料(《分離導入規格》第一節鐵律 2)。
L1 範本要「換一家公司照樣成立」;上面這些行只對某一家成立。
怎麼修(擇一,別選第四條「把詞換掉了事」):
1. 只是註解/舉例 → 改寫成通用敘述(例:「某個 repo」而非具體專案名)
2. 其實是政策包的內容 → 搬進 policy pack(L2),不要留在框架
3. 真的暫時搬不走 → 行尾加註記 $EXEMPT_MARK + 一句「為什麼還不能拿掉」
⚠️ 不准做的:把專名換成模糊詞讓檢查過關——資訊消失了,分層問題還在。
已豁免(不計違規):$EXEMPTED 行
EOF
exit 1
fi
echo "✅ 範本區無未豁免的實例專名(已豁免 $EXEMPTED 行,記在帳上待搬遷)"
exit 0
+231 -98
View File
@@ -36,29 +36,49 @@ SCRIPTS_URL="$TEMPLATE_SOURCE/scripts"
CREATED=()
SKIPPED=()
# ── 解析模組參數 ──────────────────────────────────
# ── 解析模組 / profile 參數 ────────────────────────
# profile scope 軸(這個資料夾是「成員 repo」還是「總管」)。
# 它決定裝哪一部憲法——agent 在哪裡醒來就只讀得到哪部,**不靠 agent 自我判斷**。
#
# ⚠️ 契約在檔案,不在這支腳本:真正的 scope 真相源是 `system-dev/.profile`。
# 本腳本只是**其中一個寫入者**——「把網址丟給 AI 幫我裝」那條路(主打入口)
# 同樣必須寫出這個檔,格式一致即可互通。腳本入口是備援與參考實作,不是唯一路徑。
MODULE=""
PROFILE=""
for arg in "$@"; do
case "$arg" in
--wiki|--wiki-only) MODULE="wiki" ;;
--sdd|--sdd-only) MODULE="sdd" ;;
--all) MODULE="all" ;;
--wiki|--wiki-only) MODULE="wiki" ;;
--sdd|--sdd-only) MODULE="sdd" ;;
--all) MODULE="all" ;;
--profile=repo) PROFILE="repo" ;;
--profile=orchestrator) PROFILE="orchestrator" ;;
--profile=*)
echo "❌ 不認得的 profile${arg#--profile=}(只接受 repo / orchestrator" >&2
exit 1 ;;
-h|--help)
if [ "$IS_ZH" = "yes" ]; then
cat <<'HELP'
用法:install.sh [--wiki | --sdd | --all]
用法:install.sh [--wiki | --sdd | --all] [--profile=repo|orchestrator]
--wiki 只裝 LLM WikiCC 記憶系統 + 機敏防護)
--sdd 只裝 SDD 系統(動 code 前強制要有設計文件)
--all 兩個都裝(預設)
無參數 互動式詢問要裝哪個
--profile=repo 成員 repo:實際寫 code 的子專案(預設)
--profile=orchestrator 總管:管一群成員 repo 的上層資料夾
未指定 → 自動偵測並請你確認一次(結果寫進 system-dev/.profile,之後不再問)
HELP
else
cat <<'HELP'
Usage: install.sh [--wiki | --sdd | --all]
Usage: install.sh [--wiki | --sdd | --all] [--profile=repo|orchestrator]
--wiki Install LLM Wiki only (CC memory system + secret protection)
--sdd Install SDD system only (require a design doc before touching code)
--all Install both (default)
no flag Interactively ask which to install
--profile=repo Member repo: a project where code actually gets written (default)
--profile=orchestrator Orchestrator: the folder that manages several member repos
unset -> auto-detect and ask once (result stored in system-dev/.profile)
HELP
fi
exit 0 ;;
@@ -107,6 +127,57 @@ echo ""
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
echo ""
# ── 決定 profile(scope 軸)──────────────────────────
# 必須在**寫任何檔案之前**問完(EARS-1.1.3)——裝了一半才問是最差的體驗。
#
# 偵測規則:目前目錄下有多個「各自帶 .git 的子目錄」= 這裡像是管著一群 repo 的上層
# → 建議 orchestrator。偵測只給建議,**仍要人確認一次**(一次性,之後寫進 marker 檔)。
detect_profile() {
local n=0 d
for d in */; do
[ -d "$d/.git" ] && n=$((n + 1))
[ "$n" -ge 2 ] && break
done
if [ "$n" -ge 2 ]; then printf 'orchestrator'; else printf 'repo'; fi
}
if [ -z "$PROFILE" ]; then
SUGGESTED="$(detect_profile)"
if [ -t 0 ]; then
echo ""
t "🧭 這個資料夾是哪一種?(決定裝哪一部憲法)" \
"🧭 What is this folder? (decides which constitution gets installed)"
t " 1) 成員 repo —— 實際寫 code 的子專案" \
" 1) Member repo — a project where code actually gets written"
t " 2) 總管 —— 管一群成員 repo 的上層資料夾" \
" 2) Orchestrator — the folder that manages several member repos"
echo ""
if [ "$SUGGESTED" = "orchestrator" ]; then
t " (偵測到底下有多個各自獨立的 repo → 看起來像 2)" \
" (found multiple independent repos below -> looks like 2)"
else
t " (沒偵測到多個獨立 repo → 看起來像 1)" \
" (no multiple independent repos found -> looks like 1)"
fi
tn "請輸入 1 / 2 [預設:$SUGGESTED]" "Enter 1 / 2 [default: $SUGGESTED]: "
read -r pchoice || pchoice=""
case "$pchoice" in
1) PROFILE="repo" ;;
2) PROFILE="orchestrator" ;;
*) PROFILE="$SUGGESTED" ;;
esac
else
# 非互動(curl | bash 無 tty)→ 用偵測值,並在結尾大聲說它是怎麼決定的
PROFILE="$SUGGESTED"
t "🧭 非互動環境 → 自動判定 profile:$PROFILE(可事後改 system-dev/.profile" \
"🧭 Non-interactive -> auto-detected profile: $PROFILE (change system-dev/.profile later)"
fi
fi
echo ""
t "🧭 安裝 profile$PROFILE" "🧭 Profile: $PROFILE"
echo ""
# ── 重複安裝防呆(1.10.1):install 只管「全新安裝」,一切後續歸 update ──
# 判準是「裝過沒」,不分新版舊版:
# - 新結構 system-dev/ 已存在,或
@@ -243,97 +314,93 @@ download_if_missing() {
fi
}
# ── 共用結構 ──────────────────────────────────────
# 工具自己的文件骨架收進 system-dev/docs/(不污染用戶根目錄、不跟用戶自己的 docs/ 混)。
# 注意語義分離:這裡的 system-dev/docs/ 是「工具文件」;用戶的 raw source(原始文件)
# 另有其處(見上方 vault 偵測),工具只讀、不搬。
# .claude/ 只留 CC 死綁的 commands/ + hooks/,工具資料一律不放這
create_dir "system-dev/docs/1-vision"
create_dir "system-dev/docs/2-architecture/decisions"
create_dir "system-dev/docs/4-guides"
create_dir "system-dev/docs/5-records/incidents"
create_dir "system-dev/docs/5-records/test-reports"
create_dir "system-dev/docs/6-user"
create_dir ".claude/commands"
create_dir ".claude/hooks"
download_if_missing "system-dev/docs/README.md" "$REPO_URL/system-dev/docs/README.md"
# ── Manifest 驅動的安裝(取代原本硬編的檔案清單)──────────────
# 為什麼改成這樣(SDD jdd-dual-profile 決策 D1):
# 原本清單硬編在這裡,update.sh 另有一份幾乎重複的硬編。兩份手抄必然漂移,
# 而且**已經漂了**——install 從來不裝 wiki-first-search.sh / subagent-wiki-guard.sh /
# publish-lag-check.sh / decisions-summary.md,但 update 會裝/會保留
# ⇒ 乾淨安裝的人反而拿不到最新三版的招牌功能。
# 現在兩支腳本讀同一份 manifest,這類漂移在結構上不可能再發生。
#
# manifest 欄位:src \t dest \t class \t module \t profile(詳見 template/manifest/common.tsv
# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project
# 投影(tasks-project-sync)共用同一套解析,別各寫一份。放共用區,兩模組都拿得到。
download_if_missing "system-dev/docs/4-guides/logseq-markers.md" "$REPO_URL/system-dev/docs/4-guides/logseq-markers.md"
MANIFEST_DIR="system-dev/.template-manifest.d"
mkdir -p "$MANIFEST_DIR"
# 工具版號:放 system-dev/,不寄生 .claude/。
download_if_missing "system-dev/VERSION" "$REPO_URL/system-dev/VERSION"
fetch_manifest() { # $1=名稱(common|repo|orchestrator
local name="$1" out="$MANIFEST_DIR/$1.tsv"
if curl -sSL "$REPO_URL/manifest/$name.tsv" -o "$out" 2>/dev/null && [ -s "$out" ]; then
return 0
fi
rm -f "$out"
return 1
}
# ── WIKI 模組 ─────────────────────────────────────
# wiki 是工具資料 → 放 system-dev/wiki/(不放 .claude/)。
# commands/ 與 hooks/ 是 CC 機制檔 → 維持 .claude/。
if $WANT_WIKI; then
create_dir "system-dev/wiki"
download_if_missing "system-dev/wiki/INDEX.md" "$REPO_URL/system-dev/wiki/INDEX.md"
download_if_missing "system-dev/wiki/TAXONOMY.md" "$REPO_URL/system-dev/wiki/TAXONOMY.md"
download_if_missing "system-dev/wiki/status.md" "$REPO_URL/system-dev/wiki/status.md"
download_if_missing "system-dev/wiki/mistakes.md" "$REPO_URL/system-dev/wiki/mistakes.md"
download_if_missing "system-dev/wiki/principles.md" "$REPO_URL/system-dev/wiki/principles.md"
download_if_missing "system-dev/wiki/.wikiignore" "$REPO_URL/system-dev/wiki/.wikiignore"
if ! fetch_manifest common; then
echo "❌ 抓不到安裝清單($REPO_URL/manifest/common.tsv)——網路或來源網址有問題,中止。" >&2
exit 1
fi
fetch_manifest "$PROFILE" || true # profile 專屬清單可以是空的
# wiki 改寫產物(AI 自讀定稿卡片)的正式落點:由工具建好,不靠用戶自救。
create_dir "system-dev/wiki/cards"
# src 前綴 → 實際來源網址
resolve_src() { # $1=src 欄
case "$1" in
T:*) printf '%s/%s' "$REPO_URL" "${1#T:}" ;;
S:*) printf '%s/%s' "$SCRIPTS_URL" "${1#S:}" ;;
*) printf '' ;;
esac
}
# 要不要裝這一行(依模組)
want_module() { # $1=module 欄
case "$1" in
core) return 0 ;;
wiki) $WANT_WIKI && return 0 || return 1 ;;
sdd) $WANT_SDD && return 0 || return 1 ;;
*) return 1 ;;
esac
}
# 逐行安裝。CLAUDE.mdclass=claude-md)不在這裡處理——它要三段組裝,走下方專段。
install_from_manifest() { # $1=manifest 檔
local f="$1" src dest class module profile url
[ -f "$f" ] || return 0
while IFS=$'\t' read -r src dest class module profile; do
case "$src" in ''|'#'*) continue ;; esac
[ -z "${dest:-}" ] && continue
want_module "$module" || continue
case "$class" in
dir)
create_dir "$dest"
;;
claude-md)
CLAUDE_MD_SRC="$(resolve_src "$src")" # 交給下方 CLAUDE.md 組裝段
;;
*)
url="$(resolve_src "$src")"
[ -z "$url" ] && continue
download_if_missing "$dest" "$url"
;;
esac
done < "$f"
}
CLAUDE_MD_SRC=""
install_from_manifest "$MANIFEST_DIR/common.tsv"
install_from_manifest "$MANIFEST_DIR/$PROFILE.tsv"
# wiki 卡片落點的 .gitkeep(目錄由 manifest 建,這顆種子檔留在腳本裡)
if $WANT_WIKI && [ -d "system-dev/wiki/cards" ]; then
[ -f "system-dev/wiki/cards/.gitkeep" ] || { : > "system-dev/wiki/cards/.gitkeep"; CREATED+=("system-dev/wiki/cards/.gitkeep"); }
download_if_missing ".claude/commands/wiki-init.md" "$REPO_URL/.claude/commands/wiki-init.md"
download_if_missing ".claude/commands/wiki-capture.md" "$REPO_URL/.claude/commands/wiki-capture.md"
download_if_missing ".claude/commands/wiki-update.md" "$REPO_URL/.claude/commands/wiki-update.md"
download_if_missing ".claude/commands/wiki-recall.md" "$REPO_URL/.claude/commands/wiki-recall.md"
# vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):給 Routine 反覆跑。
download_if_missing ".claude/commands/wiki-extract.md" "$REPO_URL/.claude/commands/wiki-extract.md"
# wiki 相關 hooks:接關 + 機敏掃描
download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh"
download_if_missing ".claude/hooks/wiki-secret-scan.sh" "$REPO_URL/.claude/hooks/wiki-secret-scan.sh"
# Coworkclaude.ai)整理 wiki 用的 skill:與 CC 的 /wiki-init 共用同一套規則
# (含 typed-edge、frontmatter 標籤、gloss)。沒這支 → claude.ai 來掃時身上沒規則。
download_if_missing "system-dev/docs/SKILL.md" "$REPO_URL/system-dev/docs/SKILL.md"
fi
# ── SDD 模組 ──────────────────────────────────────
if $WANT_SDD; then
create_dir "system-dev/docs/3-specs"
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
download_if_missing "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$REPO_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md"
download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh"
# SDD 生命週期鐵律(1.14,issue #6):規則真相源 + 規格變更緩衝區 + 獨立單一活性檢查
download_if_missing "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$REPO_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md"
download_if_missing "system-dev/docs/3-specs/pending-changes.md" "$REPO_URL/system-dev/docs/3-specs/pending-changes.md"
download_if_missing "system-dev/scripts/sdd-active-check.sh" "$REPO_URL/scripts/sdd-active-check.sh"
# ── tasks⇄Project 投影(optionalissue #16)──────────────────
# 帶檔 ≠ 啟用:workflow yaml 只是「留記錄+手動啟用素材」,啟用=對話答好且 acr push。
# 投影邏輯依附 tasks.md(住 3-specs),故隨 SDD 模組帶下來;裝了不代表開。
create_dir "system-dev/workflows"
download_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$REPO_URL/system-dev/workflows/tasks-project-sync.yaml"
download_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$REPO_URL/system-dev/workflows/tasks-project-sync.local.sh"
fi
# ── 安裝/更新腳本:一開始就放進 system-dev/scripts/ ──
# 為什麼一開始就裝:之後要更新,用戶(或 CC)直接 `bash system-dev/scripts/update.sh`
# 不必每次都記那串 curl。腳本來源在 main/scripts/(不在 template/)。
create_dir "system-dev/scripts"
download_if_missing "system-dev/scripts/install.sh" "$SCRIPTS_URL/install.sh"
download_if_missing "system-dev/scripts/update.sh" "$SCRIPTS_URL/update.sh"
# ── 共用 hook:專案自訂禁令骨架(預設停用)────────
download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh"
# ── 共用指引:GitHub issue 處理(讀/回普世,跨 repo 發要先問,禁自動輪詢)──
download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md"
# ── scope 軸 marker:這個實例是哪一種 ────────────────
# 這是 role-lib.sh 判 scope 的**唯一**來源。AI 代裝路徑也必須寫出同格式的檔。
printf '%s\n' "$PROFILE" > "system-dev/.profile"
CREATED+=("system-dev/.profile ($PROFILE)")
chmod +x .claude/hooks/*.sh 2>/dev/null || true
chmod +x .claude/hooks/lib/*.sh 2>/dev/null || true
chmod +x system-dev/workflows/*.sh 2>/dev/null || true
chmod +x system-dev/scripts/*.sh 2>/dev/null || true
@@ -346,7 +413,9 @@ build_hooks_json() {
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
fi
# PreToolUse 依模組疊加
# PreToolUse 依模組疊加
# 順序原則:**範圍大的擋在前**——這樣錯誤訊息會指向最根本的那條規則,
# 而不是讓人先修一個表層問題、修完才發現底下還有一條。
local pt=()
$WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
@@ -354,7 +423,15 @@ build_hooks_json() {
local IFS=,
pretool_hooks="${pt[*]}"
printf '{\n "hooks": {\n'
# role 軸預設值:由 profile 決定,寫進 settings.json 的 env。
# 為什麼要寫死一個預設:AGENT_ROLE 沒設時 role-lib.sh 會「依 scope 推定」,
# 但推定是保險不是設計——明寫出來,人才看得見自己這個實例預設是什麼身分。
local default_role="engineer"
[ "$PROFILE" = "orchestrator" ] && default_role="orchestrator"
printf '{\n'
printf ' "env": { "AGENT_ROLE": "%s" },\n' "$default_role"
printf ' "hooks": {\n'
if [ -n "$session_hooks" ]; then
printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks"
fi
@@ -369,19 +446,75 @@ else
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
fi
# ── CLAUDE.md只在完全不存在時建立 ────────────────
# 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入,
# 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。
# ── CLAUDE.md由 profile 範本三段組裝 ────────────────
# 結構(界標用 HTML 註解:md 渲染看不見、grep 定位得到、CC 讀得到):
#
# <!-- sdt:framework begin profile=X version=Y sha256=Z -->
# (profile 憲法範本原文,一字不改)
# <!-- sdt:framework end -->
# <!-- sdt:local begin -->
# raw source 宣告 + 之後使用者/CC 自由追加)
# <!-- sdt:local end -->
#
# 為什麼要界標:在這之前 CLAUDE.md 是「整份下載 append」,**沒有任何邊界** ⇒
# update 無從分辨「這段是框架的、那段是你寫的」,因此永遠不敢覆蓋,
# 框架改了憲法也送不到既有實例;而使用者手改框架段也沒人看得見。
# 有了界標+sha256,兩件事同時解決:框架段可安全更新、被手改時抓得到(漂移偵測)。
sdt_sha256() { # 跨平台取 sha256macOS 用 shasumLinux 多為 sha256sum
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
}
if [ ! -f "CLAUDE.md" ]; then
download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md"
if [ -f "CLAUDE.md" ]; then
emit_raw_source_block >> CLAUDE.md
CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE}" "← raw source declaration written (${VAULT_TYPE})")")
if [ -z "$CLAUDE_MD_SRC" ]; then
echo "⚠️ manifest 沒有指定 $PROFILE 的 CLAUDE.md 範本,略過憲法安裝" >&2
else
FW_TMP="$(mktemp)"
if curl -sSL "$CLAUDE_MD_SRC" -o "$FW_TMP" 2>/dev/null && [ -s "$FW_TMP" ]; then
FW_SHA="$(sdt_sha256 "$FW_TMP")"
TOOL_VER="$(tr -d '[:space:]' < system-dev/VERSION 2>/dev/null || echo 'unknown')"
{
printf '<!-- sdt:framework begin profile=%s version=%s sha256=%s -->\n' \
"$PROFILE" "$TOOL_VER" "$(printf '%s' "$FW_SHA" | cut -c1-12)"
cat "$FW_TMP"
printf '<!-- sdt:framework end -->\n\n'
printf '<!-- sdt:local begin — 這一區是你的,update 永遠不會動它 -->\n'
emit_raw_source_block
printf '\n<!-- sdt:local end -->\n'
} > CLAUDE.md
rm -f "$FW_TMP"
CREATED+=("CLAUDE.md $(tn "${PROFILE} 憲法 本地補充區(raw source: ${VAULT_TYPE}" "${PROFILE} constitution + local section (raw source: ${VAULT_TYPE})")")
else
rm -f "$FW_TMP"
echo "⚠️ 抓不到 $PROFILE 憲法範本($CLAUDE_MD_SRC),CLAUDE.md 未建立" >&2
fi
fi
else
SKIPPED+=("CLAUDE.md $(tn '(已存在,請手動加入對應區塊)' '(already exists — add the block manually)')")
SKIPPED+=("CLAUDE.md $(tn '(已存在,未覆蓋——要導入 profile 憲法請跑 update.sh)' '(already exists — run update.sh to adopt the profile constitution)')")
fi
# ── 產生 .template-manifest(漂移偵測的基準)────────────
# 記下每個安裝產物「安裝當下」的雜湊。update 時比對:
# 實檔 sha == 這裡的 sha → 乾淨,可安全覆蓋成新版
# 實檔 sha != 這裡的 sha → **被手改過**,不覆蓋、列進漂移清單
# 沒有這張表,就只能「不敢覆蓋」或「盲目覆蓋」二選一,兩個都錯。
{
printf '# dest\tclass\tversion\tsha256 —— 安裝當下的快照,供 update 判漂移用,勿手改\n'
TOOL_VER="$(tr -d '[:space:]' < system-dev/VERSION 2>/dev/null || echo 'unknown')"
for mf in "$MANIFEST_DIR/common.tsv" "$MANIFEST_DIR/$PROFILE.tsv"; do
[ -f "$mf" ] || continue
while IFS=$'\t' read -r m_src m_dest m_class m_module m_profile; do
case "$m_src" in ''|'#'*) continue ;; esac
[ -z "${m_dest:-}" ] && continue
[ "$m_class" = "dir" ] && continue
[ -f "$m_dest" ] || continue
printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$TOOL_VER" "$(sdt_sha256 "$m_dest")"
done < "$mf"
done
} > system-dev/.template-manifest
CREATED+=("system-dev/.template-manifest")
# ── 輸出結果 ──────────────────────────────────────
echo ""
t "✅ 建立了:" "✅ Created:"
+27
View File
@@ -0,0 +1,27 @@
# instance-names.txt — 實例專名黑名單(機械閘 #2 的判準來源)
#
# 鐵律(《分離導入規格》第一節鐵律 2):
# **框架不含實例資料**——L1 範本檔出現具體專案名(arcrun、mira、leo21c…)=格式錯誤。
# 換一家公司照樣成立的才配住在框架裡;換一家公司就不成立的,那是 L2 政策包或 L3 實例的事。
#
# 用法:scripts/check-no-instance-names.sh 掃 template/(範本區),命中即 fail。
#
# ── 豁免(唯一合法的例外,留痕可審)──────────────────────
# 在命中行的行尾加註記: sdt-instance-name-ok
# 規矩:豁免必須附一句「為什麼還不能拿掉」,且應該有對應的搬遷計畫。
# 豁免不是赦免——它是「這行欠著,記在帳上」。
#
# ── 不受此檢查的地方 ────────────────────────────────
# · policy pack(L2 政策包本來就是一家之言,專名是它的內容不是它的錯)
# · 本 repo 自己的工作現場:docs/、system-dev/、CHANGELOG.md、README*、scripts/
# (那些是「我們怎麼做出來的」,不是發給別人的範本)
#
# 一行一個詞,大小寫不敏感。# 開頭為註解。
arcrun
mira
leo21c
inkstone
uncle6
polaris
richblack
+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"