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>
This commit is contained in:
2026-08-06 00:26:56 +08:00
parent 6a49f25aef
commit 2f5d9f3bb2
18 changed files with 1246 additions and 83 deletions
+177 -56
View File
@@ -298,67 +298,148 @@ echo ""
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh"
# ── 舊版硬編清單(僅在抓不到 manifest 時使用的退路)──────────
# 保留它的唯一理由:來源不可達時,更新不該整個停擺。
# ⚠️ 新增產物請改 template/manifest/*.tsv**不要**再往這裡加——
# 這份清單就是當初漂移的來源。
legacy_update_list() {
# ── 模板/邏輯檔:覆蓋更新 ──────────────────────────
# 共用 hook 與指引
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project
# 投影共用同一套;任一模組在用就補/更新(邏輯檔,可覆蓋)。
if $HAS_WIKI || $HAS_SDD; then
update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md"
if $HAS_WIKI || $HAS_SDD; then
update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md"
fi
if $HAS_WIKI; then
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/hooks/commands 留 .claude/。
update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md"
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
# 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG)
update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh"
update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh"
# 1.18.0:公開 mirror 落後偵測(手動同步的靜默失敗只有外部用戶會撞到)
update_file ".claude/hooks/publish-lag-check.sh" "$TEMPLATE_URL/.claude/hooks/publish-lag-check.sh"
update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md"
update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md"
update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md"
update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md"
# vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):邏輯檔,可覆蓋。舊版沒有 → 當新檔補。
update_file ".claude/commands/wiki-extract.md" "$TEMPLATE_URL/.claude/commands/wiki-extract.md"
# Coworkclaude.ai)的 wiki 整理 skill:規則檔,可覆蓋
update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md"
# wiki 的「使用者資料」:絕不碰
keep_file "system-dev/wiki/status.md"
keep_file "system-dev/wiki/mistakes.md"
# principles.md1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留
add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md"
keep_file "system-dev/wiki/decisions-summary.md"
keep_file "system-dev/wiki/TAXONOMY.md"
keep_file "system-dev/wiki/.wikiignore"
fi
if $HAS_SDD; then
# SDD 範本與 hook:可覆蓋
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
update_file "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md"
update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh"
# SDD 生命週期鐵律(1.14,issue #6):規則檔+獨立檢查腳本=邏輯檔可覆蓋;
# pending-changes.md 裝著用戶的 proposal=用戶資料,只補不覆蓋。
# issue #13 教訓:update 不補新檔會造成結構斷層,新檔必須在這裡鋪。)
update_file "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$TEMPLATE_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md"
add_if_missing "system-dev/docs/3-specs/pending-changes.md" "$TEMPLATE_URL/system-dev/docs/3-specs/pending-changes.md"
update_file "system-dev/scripts/sdd-active-check.sh" "$TEMPLATE_URL/scripts/sdd-active-check.sh"
# tasks⇄Project 投影(issue #16):邏輯檔,可覆蓋。舊版沒有 → add_if_missing 補。
# 啟用狀態存遠端(acr push),不在這些檔裡,覆蓋不會關掉誰的同步。
add_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.yaml"
add_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.local.sh"
fi
}
# ── Manifest 驅動的更新(1.19.0:兩支腳本讀同一份清單)────────
# 在這之前,這裡是一份**手抄的**檔案清單,install.sh 另有一份。兩份手抄必然漂移,
# 而且已經漂了:wiki-first-search / subagent-wiki-guard / publish-lag-check /
# decisions-summary 這四個,update 會處理但 install 從來不裝
# ⇒ 乾淨安裝的人反而拿不到最新三版的招牌功能。
# 現在兩支都讀 template/manifest/*.tsv,這類漂移在結構上不可能再發生。
#
# 讀不到 manifest(來源太舊/網路問題)→ 退回舊的硬編清單(見下方 legacy_update_list),
# 不讓更新整個停擺。
MANIFEST_DIR="system-dev/.template-manifest.d"
mkdir -p "$MANIFEST_DIR"
SDT_PROFILE="repo"
[ -f "system-dev/.profile" ] && SDT_PROFILE="$(tr -d '[:space:]' < system-dev/.profile 2>/dev/null || echo repo)"
fetch_manifest() { # $1=名稱
local out="$MANIFEST_DIR/$1.tsv"
if curl -sSL "$TEMPLATE_URL/manifest/$1.tsv" -o "$out" 2>/dev/null \
&& [ -s "$out" ] && ! looks_like_error_page "$out"; then
return 0
fi
rm -f "$out"; return 1
}
resolve_src() {
case "$1" in
T:*) printf '%s/%s' "$TEMPLATE_URL" "${1#T:}" ;;
S:*) printf '%s/%s' "$REPO_RAW/scripts" "${1#S:}" ;;
*) printf '' ;;
esac
}
want_module() {
case "$1" in
core) return 0 ;;
wiki) $HAS_WIKI && return 0 || return 1 ;;
sdd) $HAS_SDD && return 0 || return 1 ;;
*) return 1 ;;
esac
}
apply_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
url="$(resolve_src "$src")"
case "$class" in
dir) mkdir -p "$dest" 2>/dev/null || true ;;
overwrite) [ -n "$url" ] && update_file "$dest" "$url" ;;
add-if-missing) [ -n "$url" ] && add_if_missing "$dest" "$url" ;;
keep-with-template) [ -n "$url" ] && keep_with_template "$dest" "$url" ;;
keep) keep_file "$dest" ;;
claude-md) CLAUDE_MD_SRC="$url" ;; # 憲法走界標補植,見下方專段
*) : ;;
esac
done < "$f"
}
CLAUDE_MD_SRC=""
USED_MANIFEST=false
if fetch_manifest common; then
USED_MANIFEST=true
fetch_manifest "$SDT_PROFILE" || true
apply_manifest "$MANIFEST_DIR/common.tsv"
apply_manifest "$MANIFEST_DIR/$SDT_PROFILE.tsv"
else
t "⚠️ 抓不到安裝清單,退回舊版硬編清單(功能較少但不會壞)" \
"⚠️ Manifest unavailable; falling back to the legacy hardcoded list"
legacy_update_list
fi
if $HAS_WIKI; then
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/hooks/commands 留 .claude/。
update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md"
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
# 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG)
update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh"
update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh"
# 1.18.0:公開 mirror 落後偵測(手動同步的靜默失敗只有外部用戶會撞到)
update_file ".claude/hooks/publish-lag-check.sh" "$TEMPLATE_URL/.claude/hooks/publish-lag-check.sh"
update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md"
update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md"
update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md"
update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md"
# vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):邏輯檔,可覆蓋。舊版沒有 → 當新檔補。
update_file ".claude/commands/wiki-extract.md" "$TEMPLATE_URL/.claude/commands/wiki-extract.md"
# Coworkclaude.ai)的 wiki 整理 skill:規則檔,可覆蓋
update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md"
# wiki 的「使用者資料」:絕不碰
keep_file "system-dev/wiki/status.md"
keep_file "system-dev/wiki/mistakes.md"
# principles.md1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留
add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md"
keep_file "system-dev/wiki/decisions-summary.md"
keep_file "system-dev/wiki/TAXONOMY.md"
keep_file "system-dev/wiki/.wikiignore"
fi
if $HAS_SDD; then
# SDD 範本與 hook:可覆蓋
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
update_file "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
update_file "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md"
update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh"
# SDD 生命週期鐵律(1.14,issue #6):規則檔+獨立檢查腳本=邏輯檔可覆蓋;
# pending-changes.md 裝著用戶的 proposal=用戶資料,只補不覆蓋。
# issue #13 教訓:update 不補新檔會造成結構斷層,新檔必須在這裡鋪。)
update_file "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$TEMPLATE_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md"
add_if_missing "system-dev/docs/3-specs/pending-changes.md" "$TEMPLATE_URL/system-dev/docs/3-specs/pending-changes.md"
update_file "system-dev/scripts/sdd-active-check.sh" "$TEMPLATE_URL/scripts/sdd-active-check.sh"
# tasks⇄Project 投影(issue #16):邏輯檔,可覆蓋。舊版沒有 → add_if_missing 補。
# 啟用狀態存遠端(acr push),不在這些檔裡,覆蓋不會關掉誰的同步。
add_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.yaml"
add_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.local.sh"
fi
# ── 自我更新:把最新的 update.sh / install.sh 抓到 system-dev/scripts/ ──
# 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。
update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh"
@@ -368,7 +449,47 @@ chmod +x .claude/hooks/*.sh system-dev/scripts/*.sh system-dev/workflows/*.sh 2>
# ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」──
keep_file ".claude/settings.json"
keep_file "CLAUDE.md"
# ── CLAUDE.md 界標補植(1.19.0)────────────────────────
# 1.18.x 以前的 CLAUDE.md 是「整份下載 + 往後 append」,**沒有任何區段界標**。
# 沒界標 ⇒ update 分不出「哪段是框架的、哪段是你寫的」⇒ 只能永遠不敢碰
# ⇒ 框架後來改的憲法**永遠送不到既有實例**。這一段就是來解這個死結。
#
# 保守到底,寧可什麼都不做也不弄丟一個字:
# · 已有界標 → 什麼都不做(冪等)
# · 沒有界標 → 現有全文**原封不動**包進本地補充區,框架區重鋪在它上面
# · 抓不到憲法範本 → 放棄補植、保留原檔(不留半殘的檔)
# 最壞情況是同一段內容在兩區各一份,由人自己刪——我們不代刪。
if [ -f "CLAUDE.md" ]; then
if grep -q 'sdt:framework begin' CLAUDE.md 2>/dev/null; then
keep_file "CLAUDE.md"
elif [ -n "${CLAUDE_MD_SRC:-}" ]; then
FW_TMP="$(mktemp)"
if curl -sSL "$CLAUDE_MD_SRC" -o "$FW_TMP" 2>/dev/null && ! looks_like_error_page "$FW_TMP"; then
cp CLAUDE.md "CLAUDE.md.before-markers"
{
printf '<!-- sdt:framework begin profile=%s version=%s sha256=%s -->\n' \
"$SDT_PROFILE" "$REMOTE_VER" "$(sdt_sha256 "$FW_TMP" | cut -c1-12)"
cat "$FW_TMP"
printf '<!-- sdt:framework end -->\n\n'
printf '<!-- sdt:local begin — 這一區是你的,update 永遠不會動它 -->\n'
printf '\n<!-- ⬇️ 以下是你原本的 CLAUDE.md,一字未動地搬進來。\n'
printf ' 上面的框架區從現在起由 template 維護;兩邊若有重複的段落,\n'
printf ' 請自己刪掉這裡的舊版本(我們不敢代你刪)。\n'
printf ' 原檔備份:CLAUDE.md.before-markers -->\n\n'
cat "CLAUDE.md.before-markers"
printf '\n<!-- sdt:local end -->\n'
} > CLAUDE.md
rm -f "$FW_TMP"
MIGRATED+=("CLAUDE.md → 補上界標(原檔備份 CLAUDE.md.before-markers")
else
rm -f "$FW_TMP"
keep_file "CLAUDE.md"
fi
else
keep_file "CLAUDE.md"
fi
fi
# ── 結果輸出 ───────────────────────────────────────
echo ""