2f5d9f3bb2
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>
656 lines
30 KiB
Bash
656 lines
30 KiB
Bash
#!/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; }
|
||
|
||
# 來源預設=公開 GitHub(leo 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 Wiki(CC 記憶系統 + 機敏防護)
|
||
--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.md(class=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() { # 跨平台取 sha256(macOS 用 shasum,Linux 多為 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 讀取順序(push:hook 開 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 變動前必須有對應 SDD(system-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 查環境有沒有 Arcrun(mcp / 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 issue:CC 可直接 /issue-handle 讀回自己 repo 的 issue(禁自動輪詢)" \
|
||
" GitHub issues: CC can use /issue-handle to read issues from its own repo (no auto-polling)"
|
||
echo ""
|