Files
system-dev-template/scripts/install.sh
T
Leo 2f5d9f3bb2 feat(W2 Phase 2-3): JDD 文件範本+八條封路 hook+還清兩件舊債
SDD: docs/3-specs/jdd-dual-profile(active)。編號 task 26/33 完成,Phase 4-5 未開工。

■ Phase 2 JDD 文件範本(orchestrator profile)
範本形狀對齊「實際跑出來的那兩份」(總管已寫的 root.md 15 卡、journeys.md J-1 九站),
不是照規格憑空造:
- 卡片是巢狀 bullet(`- **P1** 🟢 …` + 子項放來源/對帳),非規格畫的平行文字行
- 站點索引**巢狀 bullet 不用表格**(表格會把層級壓平,看不出從屬)
- 兩份都保留「這卷還缺什麼(誠實記)」收尾段——規格沒有,但那是防假綠的地方
新增:root.md / journeys.md / sprint.md / triage-map.md 四範本(add-if-missing,
填了就永不覆蓋)+ plugin-load-order.md(W3 插槽,框架不發明平行外掛格式)

■ Phase 3 封路 hook(八條規則落六支檔)
- role-guard(J1+J2+J3)★命門:考生不能改考卷。六組實測含「考題藏在別的 md 裡」也擋
- jdd-format-guard(J4+J5+J8):紅卡缺對帳日/任務缺站號/PM 文件混技術名詞
- station-done-guard(J6):收工判準是站的考題全綠,不是任務全關
- regression-scope(J7):動實作 → 列出要重考哪幾題(只提醒不擋)
- install-artifact-guard(S1):實例不改機制
- orchestrator-scope-guard(S4):總管不進成員 repo 動實作(從實例上收進框架,
  路徑清單改由實例自填,範本零專名)
掛載鏈依「範圍大的擋在前」:改機制 → 角色 → 位置 → 格式 → 既有三支

■ 還清兩件舊債
- update.sh 檔案清單改讀 manifest(舊硬編降為抓不到來源時的 fallback)
  ——install/update 兩份手抄清單漂移的根因全修
- CLAUDE.md 界標補植:舊實例全文原封包進本地區、框架區重鋪、原檔備份、冪等
  ——解開「沒界標⇒不敢覆蓋⇒框架改的憲法永遠送不到既有實例」這個死結

■ 修掉三個自己造的問題(實測抓出來的,不是想出來的)
- jdd-format-guard 誤擋真實 journeys.md 的「這卷還缺什麼」自述段
  → 排除法改**正面圈定**(只掃卡片本體與站內文),說明區/自述段/索引自然不在範圍
- install-artifact-guard 把 pre-write-guard.sh 也擋了——而它的錯誤訊息正叫人去改那支
  → 使用者自訂插槽列為最優先放行
- check-legacy-paths 用 HEAD 當基準會**自我弱化**:改成清單驅動後保護範圍 35→29 條
  → 基準改指最後一次真正發佈的版本

■ 實測(全部貼過輸出)
- G2 考生改考卷:6/6,含 orchestrator 寫 code/engineer 改考題/考題藏別處
- G4 憲法分流:兩環境重裝,總管版技術軌關鍵字 0、成員版上游指針 8,界標 4/4
- G6 實例改機制:4/4,含框架開發標記放行與自訂插槽放行
- G7 CI 擋實例名:注入違規 → 指出檔案行號 exit 1
- G3 進度以站計量:起牀推「J-1 已點亮 2/9 站」、收工列未亮站並禁用任務數當理由
- 回歸考、界標補植冪等、orchestrator-scope-guard 四組:全通

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:26:56 +08:00

656 lines
30 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# system-dev-template installer
# 已有專案接入腳本——只建立缺少的東西,已有的一律不動。
#
# 模組化安裝:
# --wiki 只裝 LLM Wiki(記憶系統 + 機敏防護)
# --sdd 只裝 SDD 系統(動 code 前必須有 design.md
# --all 兩個都裝(預設)
# 無參數 互動式詢問
#
# 為什麼留在同一個 repo 用參數選,而不是 fork:
# 使用者多半非專業,最怕「我要去哪個 repo」。一個入口 + 選單最友善。
# 等未來功能多到 3+ 個再演進成「模板組合器」。模組邊界先在這裡劃好。
set -euo pipefail
# ── i18n:依 locale 選語言,預設英文 ──────────────────
# 為什麼預設英文:curl | bash 常是 LANG=C,外國人預設就該看得懂;
# 台灣使用者 locale 多為 zh_TW,會自動切回繁中。
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|*Hant*|*Hans*) IS_ZH="yes" ;;
*) IS_ZH="no" ;;
esac
# t "中文" "English" → 依語系印出對應字串
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
# tn = 不換行版(給 prompt 用)
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
# 來源預設=公開 GitHubleo 2026-07-21:「要發佈的正稿,從頭就不要用奇怪的網址,
# 以免改來改去」)。內部指向私有草稿源時用環境變數覆寫,不改檔:
# TEMPLATE_SOURCE=https://<私有 raw base> bash scripts/install.sh
TEMPLATE_SOURCE="${TEMPLATE_SOURCE:-https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main}"
REPO_URL="$TEMPLATE_SOURCE/template"
# install.sh / update.sh 住在 main/scripts/(不在 template/)。
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" ;;
--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] [--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] [--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 ;;
esac
done
echo ""
echo "🔧 system-dev-template installer"
echo "================================="
t "只建立缺少的目錄和檔案,已有的不動。" \
"Only creates missing dirs and files; never touches what already exists."
echo ""
# ── 無參數 → 互動式詢問(給非專業使用者)──────────
if [ -z "$MODULE" ]; then
if [ -t 0 ]; then
t "要安裝哪一塊?" "Which part do you want to install?"
t " 1) LLM Wiki —— 讓 CC 記住決策、不重複犯錯(含機敏防護)" \
" 1) LLM Wiki — let CC remember decisions and avoid repeating mistakes (with secret protection)"
t " 2) SDD —— 動 code 前強制先有設計文件" \
" 2) SDD — require a design doc before touching code"
t " 3) 兩個都裝(推薦)" " 3) Install both (recommended)"
echo ""
tn "請輸入 1 / 2 / 3 [預設 3]" "Enter 1 / 2 / 3 [default 3]: "
read -r choice || choice=3
case "$choice" in
1) MODULE="wiki" ;;
2) MODULE="sdd" ;;
*) MODULE="all" ;;
esac
else
# 非互動環境(如 curl | bash 無 tty)→ 預設全裝
MODULE="all"
fi
fi
WANT_WIKI=false
WANT_SDD=false
case "$MODULE" in
wiki) WANT_WIKI=true ;;
sdd) WANT_SDD=true ;;
all) WANT_WIKI=true; WANT_SDD=true ;;
esac
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/ 已存在,或
# - 舊結構 .claude/wiki/ 或 .claude/VERSION 存在(裝過舊版、待遷移)
# 裝過了還跑 install → 會重複建範本、甚至跟真資料並存(先 install 建空殼,遷移就被擋)。
# 正解:偵測到裝過 → 不動任何東西,導去 update(更新/遷移/補新檔都由它處理)。
if [ -d "system-dev" ] || [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ]; then
t "🛑 偵測到這個專案已經安裝過 system-dev-template。" \
"🛑 system-dev-template is already installed in this project."
t " 後續的更新、遷移、補新檔,一律由「更新腳本」處理(不要重跑 install):" \
" All updates, migrations, and new-file additions are handled by the UPDATER (don't re-run install):"
echo ""
echo " curl -sSL https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main/scripts/update.sh | bash"
echo ""
t " (重跑 install 可能建出空白範本、跟你的真資料並存,故在此停止。)" \
" (Re-running install could create empty templates alongside your real data, so it stops here.)"
exit 0
fi
# ── 偵測 vault 類型 → 決定 raw source(原始文件)路徑 ──────────
# 為什麼:這個模板原本假設「原始文件在 docs/」,但 Logseq / Obsidian
# 這種 PKM vault 有自己的目錄慣例,整理時不能照 docs/ 那套搬動,
# 否則會破壞 vault 結構、讓筆記變不可讀。
# 偵測結果寫進 CLAUDE.md,讓 CC 和未來的 Cowork skill 都知道
# 「該讀/該整理哪裡」而不是亂動。
# 必須在建立 CLAUDE.md 之前跑完。
VAULT_TYPE=""
RAW_SOURCE=""
IS_VAULT="no" # 只有 logseq/obsidian 這種「筆記軟體 vault」才算 yes
if [ -d "logseq" ]; then
VAULT_TYPE="logseq"
RAW_SOURCE="pages/, journals/"
IS_VAULT="yes"
elif [ -d ".obsidian" ]; then
VAULT_TYPE="obsidian"
RAW_SOURCE="$(tn './ (整個 vault 根目錄的 .md' './ (all .md under the vault root)')"
IS_VAULT="yes"
else
VAULT_TYPE="docs"
RAW_SOURCE="docs/"
fi
# 偵測到是筆記 vault → 出聲告訴使用者「我看到了,會小心、不破壞你的筆記結構」。
# 不是筆記(一般開發案等)→ 不囉嗦,默默把 docs/ 當原始文件夾安裝完成。
if [ "$IS_VAULT" = "yes" ]; then
t "🗂️ 偵測到 ${VAULT_TYPE} 筆記庫 → 原始文件:${RAW_SOURCE}" \
"🗂️ Detected a ${VAULT_TYPE} note vault → raw source: ${RAW_SOURCE}"
t " (會保留你筆記軟體的目錄/檔名結構,不搬動、不改名)" \
" (your note app's directory/file structure is preserved — nothing is moved or renamed)"
echo ""
fi
# 把「raw source 宣告區塊」吐出來,給新建的 CLAUDE.md append 或
# 給已存在的 CLAUDE.md 當手動補貼的提示。內容對 CC / Cowork 都是
# 機器可讀的指令(明確路徑 + 不可破壞 vault 結構的約束)。
# 寫進 CLAUDE.md 的 raw source 宣告區塊。給人也給 AI 看:
# 依 locale 只寫「一種語言」進 CLAUDE.md(雙語會讓每個 session 的 context 更滿)。
emit_raw_source_block() {
local source_kind
if [ "$IS_ZH" = "yes" ]; then
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} 筆記庫"
else source_kind="一般專案(原始文件放 raw source 路徑)"; fi
cat <<BLOCK
---
## 原始文件空間(raw source
> 安裝時偵測到的來源型態:**${source_kind}**
> CC 與 Cowork 整理/讀取「人寫的原始文件」時,**只在這裡找、只在這裡動**。
| 項目 | 值 |
|------|----|
| 來源型態 | \`${source_kind}\` |
| raw source | \`${RAW_SOURCE}\` |
**約束(CC 與 Cowork 都必須遵守)**
- 整理 wiki/知識時,原始文件**一律從上方 raw source 路徑讀取**,不要假設是 \`docs/\`。
BLOCK
if [ "$IS_VAULT" = "yes" ]; then
cat <<BLOCK
- 這是 **${VAULT_TYPE} 筆記庫**:保留它原本的目錄與檔名慣例,**不得搬動、改名、重新分類** \`.md\` 檔,
以免破壞筆記軟體結構造成筆記不可讀。整理只在 \`system-dev/wiki/\` 產出,**不動 raw source 本身**。
BLOCK
fi
else
if [ "$IS_VAULT" = "yes" ]; then source_kind="${VAULT_TYPE} note vault"
else source_kind="regular project (raw source lives at the path below)"; fi
cat <<BLOCK
---
## Raw source space
> Source type detected at install time: **${source_kind}**
> When CC and Cowork curate/read human-written raw source, **look only here and act only here**.
| Item | Value |
|------|-------|
| Source type | \`${source_kind}\` |
| raw source | \`${RAW_SOURCE}\` |
**Constraints (both CC and Cowork must obey)**
- When curating the wiki/knowledge, **always read raw source from the path above** — don't assume \`docs/\`.
BLOCK
if [ "$IS_VAULT" = "yes" ]; then
cat <<BLOCK
- This is a **${VAULT_TYPE} note vault**: keep its original directory and file-naming conventions. **Do not move, rename, or re-classify** \`.md\` files,
or you'll break the note-app structure and make notes unreadable. Curation output goes only into \`system-dev/wiki/\`; **never touch the raw source itself**.
BLOCK
fi
fi
}
# ── 工具函式 ──────────────────────────────────────
create_dir() {
if [ ! -d "$1" ]; then
mkdir -p "$1"
CREATED+=("$1/")
else
SKIPPED+=("$1/ $(tn '(已存在)' '(already exists)')")
fi
}
download_if_missing() {
local dest="$1" src="$2"
if [ ! -f "$dest" ]; then
mkdir -p "$(dirname "$dest")"
curl -sSL "$src" -o "$dest"
CREATED+=("$dest")
else
SKIPPED+=("$dest $(tn '(已存在,跳過)' '(already exists, skipped)')")
fi
}
# ── 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
MANIFEST_DIR="system-dev/.template-manifest.d"
mkdir -p "$MANIFEST_DIR"
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
}
if ! fetch_manifest common; then
echo "❌ 抓不到安裝清單($REPO_URL/manifest/common.tsv)——網路或來源網址有問題,中止。" >&2
exit 1
fi
fetch_manifest "$PROFILE" || true # profile 專屬清單可以是空的
# 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"); }
fi
# ── 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
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
build_hooks_json() {
local session_hooks="" pretool_hooks=""
if $WANT_WIKI; then
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
fi
# PreToolUse 依模組疊加。
# 順序原則:**範圍大的擋在前**——錯誤訊息才會指向最根本的那條規則,
# 而不是讓人先修一個表層問題、修完才發現底下還有一條。
# ① 改機制 這個檔你根本不該動(最外層)
# ② 角色 你這個身分不該寫這種檔
# ③ 位置 總管不該進成員 repo 動實作(僅總管實例)
# ④ 格式 檔可以寫,但寫進去的內容格式不對
# ⑤ 既有的 SDD/自訂禁令/機敏掃描
local pt=()
pt+=('{ "type": "command", "command": ".claude/hooks/install-artifact-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/role-guard.sh" }')
[ "$PROFILE" = "orchestrator" ] && pt+=('{ "type": "command", "command": ".claude/hooks/orchestrator-scope-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/jdd-format-guard.sh" }')
$WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
$WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }')
local IFS=,
pretool_hooks="${pt[*]}"
# 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
printf ' "PreToolUse": [\n { "matcher": "Write|Edit|MultiEdit",\n "hooks": [ %s ] }\n ],\n' "$pretool_hooks"
# 回歸考:動了實作之後才有意義 → PostToolUse(只提醒不擋)
printf ' "PostToolUse": [\n { "matcher": "Write|Edit|MultiEdit",\n "hooks": [ { "type": "command", "command": ".claude/hooks/regression-scope.sh" } ] }\n ],\n'
# 收工判準:站的考題全綠才算收,不是任務全關 → Stop
printf ' "Stop": [\n { "hooks": [ { "type": "command", "command": ".claude/hooks/station-done-guard.sh" } ] }\n ]\n'
printf ' }\n}\n'
}
if [ ! -f ".claude/settings.json" ]; then
build_hooks_json > .claude/settings.json
CREATED+=(".claude/settings.json $(tn "(依 $MODULE 模組產生)" "(generated for module: $MODULE)")")
else
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
fi
# ── 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
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 '(已存在,未覆蓋——要導入 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:"
# 注意:macOS bash 3.2 在 set -u 下展開「空陣列」會炸 unbound variable
# 所以這裡先確認有元素才展開(SKIPPED 區塊在下方本來就有守,CREATED 補上)。
if [ ${#CREATED[@]} -gt 0 ]; then
for item in "${CREATED[@]}"; do echo " + $item"; done
fi
if [ ${#SKIPPED[@]} -gt 0 ]; then
echo ""
t "⚠️ 跳過(已存在):" "⚠️ Skipped (already exists):"
for item in "${SKIPPED[@]}"; do echo " - $item"; done
fi
echo ""
echo "─────────────────────────────────"
# CLAUDE.md 已存在 → 依模組提醒手動加區塊
if [ -f "CLAUDE.md" ]; then
if ! grep -q "raw source" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 raw source 宣告。" \
"📌 CLAUDE.md exists but lacks a raw source declaration."
t " 請手動把以下區塊貼進去,讓 CC 與 Cowork 知道原始文件在哪、不要亂動既有結構:" \
" Paste the block below in so CC and Cowork know where the raw source is and won't disturb your structure:"
emit_raw_source_block | sed 's/^/ /'
fi
if $WANT_WIKI && ! grep -q "wiki/status.md" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 wiki 讀取順序,請手動加入:" \
"📌 CLAUDE.md exists but lacks the wiki reading order — please add it manually:"
echo ""
if [ "$IS_ZH" = "yes" ]; then
cat <<'SNIP'
## Wiki 讀取順序(pushhook 開 session 自動注入)
| 檔案 | 時機 | 用途 |
|------|------|------|
| `system-dev/wiki/status.md` | session 開始第一件事 | 當前進度 |
| `system-dev/wiki/principles.md` | 設計任何東西前 | 跨全局原則,必服從 |
| `system-dev/wiki/mistakes.md` | 做新功能前 | 已知踩坑 |
SNIP
else
cat <<'SNIP'
## Wiki reading order (push: auto-injected at session start)
| File | When | Purpose |
|------|------|---------|
| `system-dev/wiki/status.md` | first thing at session start | current progress |
| `system-dev/wiki/principles.md` | before designing anything | global principles, must obey |
| `system-dev/wiki/mistakes.md` | before building a new feature | known pitfalls |
SNIP
fi
fi
if $WANT_SDD && ! grep -q "system-dev/docs/3-specs" CLAUDE.md; then
echo ""
t "📌 CLAUDE.md 已存在但缺少 SDD 鐵律,請手動加入:" \
"📌 CLAUDE.md exists but lacks the SDD iron rule — please add it manually:"
echo ""
if [ "$IS_ZH" = "yes" ]; then
cat <<'SNIP'
## 絕對鐵律
1. 任何 code 變動前必須有對應 SDDsystem-dev/docs/3-specs/[子系統]/design.md
找不到 → 停手問負責人,不要自行建立。
SNIP
else
cat <<'SNIP'
## Iron rule
1. Every code change must have a matching SDD (system-dev/docs/3-specs/[subsystem]/design.md).
Not found → stop and ask the owner; do not create one on your own.
SNIP
fi
fi
fi
# settings.json 已存在 → 依模組提醒要合併哪些 hook
if [ -f ".claude/settings.json" ]; then
MISSING_HOOKS=()
$WANT_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING_HOOKS+=("SessionStart: session-start-recall.sh")
$WANT_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
$WANT_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING_HOOKS+=("PreToolUse(Write|Edit): sdd-guard.sh")
if [ ${#MISSING_HOOKS[@]} -gt 0 ]; then
echo ""
t "📌 .claude/settings.json 已存在,請手動把以下 hooks 合併進去(保留既有設定):" \
"📌 .claude/settings.json exists — merge the hooks below in manually (keep your existing settings):"
for h in "${MISSING_HOOKS[@]}"; do echo " • $h"; done
fi
fi
# pre-write-guard 是空殼,提醒它預設不攔(避免「以為有保護其實沒有」的安全錯覺)
echo ""
t "️ .claude/hooks/pre-write-guard.sh 是「按需手填的空插槽」,預設不攔任何東西。" \
"️ .claude/hooks/pre-write-guard.sh is an empty slot to fill on demand — by default it blocks nothing."
t " 需要專案禁令?最簡單是叫你的 CC 寫一支貼合的 guard hook(比範本表達力強);" \
" Need project-specific bans? Easiest is to ask your CC to write a tailored guard hook (more expressive than the template);"
t " 或自己填 FORBIDDEN_PATTERNS 並到 settings.json 掛上才會生效。" \
" or fill in FORBIDDEN_PATTERNS yourself and wire it into settings.json to take effect."
echo ""
t "🚀 下一步:" "🚀 Next steps:"
if $WANT_WIKI; then
t " 在 Claude Code 對話裡執行 /wiki-init" \
" In a Claude Code conversation, run /wiki-init"
t " CC 會掃描現有文件、套用 .wikiignore、建立 wiki。" \
" CC will scan your existing docs, apply .wikiignore, and build the wiki."
fi
if $WANT_SDD; then
t " 動 code 前先在 system-dev/docs/3-specs/[子系統]/ 建 design.md(可用 /sdd-check 協助)" \
" Before touching code, create design.md under system-dev/docs/3-specs/[subsystem]/ (use /sdd-check to help)"
# ── tasks⇄Project 投影:裝/init 對話 + 一次性廣告(issue #16)──
# 判準=能力(arcrun 裝了沒)+意願,不掃檔。落地成「CC 問一句」,install 只是交代 CC 去問。
echo ""
t " ❓ 待辦同步(optional,需 Arcrun):請你的 CC 問你一句——" \
" ❓ Task sync (optional, needs Arcrun): have your CC ask you once —"
t " 「您需要把本專案的待辦事項(tasks.md)同步到 GitHub 嗎?」" \
" \"Do you want this project's tasks (tasks.md) mirrored to GitHub?\""
t " 答「好」→ CC 查環境有沒有 Arcrunmcp / acr 在 PATH):有就設定同步、沒有就一次性告知" \
" Yes → CC checks for Arcrun (mcp / acr in PATH): set it up if present, otherwise inform you once"
t " 「Arcrun 是免費的 AI-friendly 工作流套件,想裝跟 Claude 說就行;之後也可手動啟用」。" \
" \"Arcrun is a free AI-friendly workflow toolkit — ask Claude to install it; you can also enable sync later.\""
t " 答「不好」→ 不做、不再追問。投影 workflow 在 system-dev/workflows/(帶檔≠啟用)。" \
" No → nothing happens, no nagging. The projection workflow sits in system-dev/workflows/ (shipped ≠ enabled)."
fi
t " GitHub issueCC 可直接 /issue-handle 讀回自己 repo 的 issue(禁自動輪詢)" \
" GitHub issues: CC can use /issue-handle to read issues from its own repo (no auto-polling)"
echo ""