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
@@ -0,0 +1,92 @@
#!/bin/bash
# install-artifact-guard.sh — 實例不准改機制(分離規格 防糾纏閘 S1)
#
# 掛 PreToolUsematcher: Write|Edit|MultiEdit)。**排在整條鏈的最前面**——
# 範圍最大的規則先擋,錯誤訊息才會指向最根本的那條。
#
# ── 擋什麼 ──────────────────────────────────────
# 寫入「安裝產物區」= 框架發下來的機制檔(hook、範本、安裝腳本、plugin 目錄)。
# 判準來自 system-dev/.template-manifest**凡是框架管的檔(class=overwrite)都算**。
# 不另寫一份路徑表——兩處維護同一條規則,遲早不同步。
#
# ── 為什麼要擋(實測數據,不是潔癖)─────────────────
# 拿一個真實使用中的實例對照框架:6 支框架 hook 裡 **4 支已被手改**
# 另有 11 支是實例自己發明的,而**沒有任何機制知道這件事**。
# 兩層後果:
# ① 框架之後修好的 bug,永遠送不到那些被手改的檔
# ② 手改的內容沒人審、也不會回饋給框架 ⇒ 同一個坑每個實例各踩一次
# 機制要改就回上游改,一次修全家;在自己家裡改,只有自己受惠、且下次更新就孤立。
#
# ── 怎麼放行 ────────────────────────────────────
# 框架 repo 自己開發時本來就要改這些檔 ⇒ 兩種放行方式:
# · repo 根目錄有 .sdt-framework-dev 檔(框架 repo 自帶並 commit,零記憶負擔)
# · 環境變數 SDT_FRAMEWORK_DEV=1(臨時情境)
# 注意:官方**沒有** --framework-dev 這個 CLI 參數(實查,非記憶),別去找。
#
# 誠實限制:擋直接寫檔。bash 繞道改檔擋不到。留痕可審,不宣稱防偽。
set -uo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0
# ── 框架開發模式 → 放行 ────────────────────────────
ROOT="$(sdt_repo_root)"
[ -f "$ROOT/.sdt-framework-dev" ] && exit 0
[ "${SDT_FRAMEWORK_DEV:-}" = "1" ] && exit 0
INPUT="$(cat)"
FILE_PATH="$(sdt_file_path "$INPUT")"
[ -z "$FILE_PATH" ] && exit 0
REL="$(sdt_rel_path "$FILE_PATH")"
MANIFEST="$ROOT/system-dev/.template-manifest"
is_managed_artifact() {
# ⓪ 使用者的自訂插槽 → 一律放行,**這一條要排在最前面**
# 它們存在的目的就是給實例填自己的規則,而且 update 不會覆蓋它們。
# (踩過:本 hook 的錯誤訊息叫人「去改 pre-write-guard.sh」,
# 結果自己把那支也擋了——叫人走的路自己堵住,是最糟的一種閘。)
case "$1" in
.claude/hooks/pre-write-guard.sh) return 1 ;;
*.template.sh|*.template.md) return 1 ;;
.claude/settings.json) return 1 ;;
esac
# ① manifest 是權威:它說這是框架管的邏輯檔(class=overwrite)才擋
if [ -f "$MANIFEST" ]; then
if awk -F'\t' -v d="$1" '!/^#/ && $1==d && $2=="overwrite" {found=1} END{exit !found}' "$MANIFEST"; then
return 0
fi
fi
# ② manifest 還沒有/這個檔還沒登記(舊實例、或框架剛加的新檔)
# → 退回路徑慣例,別因為沒表就整條規則失效
case "$1" in
.claude/hooks/*|.claude/plugins/*) return 0 ;;
system-dev/scripts/*) return 0 ;;
esac
return 1
}
if is_managed_artifact "$REL"; then
cat >&2 <<EOF
❌ BLOCKED by install-artifact-guard(防糾纏閘 S1
這個檔是**框架發下來的機制**,實例不改機制。
路徑:$REL
正確做法:
· 機制要改 → 回框架/政策包 repo 提案,bump 版本,實例再 update
(一次修全家,別人也拿得到;在這裡改只有你受惠,而且下次更新就孤立)
· 只是這個專案的特殊禁令 → 寫進 .claude/hooks/pre-write-guard.sh 的自訂區
(那支就是留給你填的插槽,不會被 update 覆蓋)
· 你就是在開發框架本身 → 這個 repo 根目錄該有 .sdt-framework-dev
為什麼擋(實測,不是原則潔癖):
對照過一個真實使用中的實例——6 支框架 hook 裡 4 支已被手改,
而沒有任何人知道。那 4 支從此收不到框架的任何修正。
EOF
exit 2
fi
exit 0
+167
View File
@@ -0,0 +1,167 @@
#!/bin/bash
# jdd-format-guard.sh — PM 軌文件的格式閘(JDD 規則 J4+J5+J8)
#
# 掛 PreToolUsematcher: Write|Edit|MultiEdit)。
#
# J4 root.md 的 🔴 卡缺【要驗證 + 對帳日】 → 擋
# 為什麼:紅卡=賭注。賭注沒有「賭輸了怎麼知道」和「什麼時候結算」,
# 就會永遠是「還在做」,永遠不必認賠——那不是賭注,是藉口。
#
# J5 tasks.md **新增**的任務缺站號 → 擋
# 為什麼:sprint 的單位是站。任務不掛站,就沒有人答得出
# 「這個任務不做,哪一站會掛?」——那正是認領流程唯一的問題。
#
# J8 root.mdjourneys.md 出現技術名詞 → 擋
# 為什麼:這兩份是給不懂技術的人讀的。技術是達成手段,寫進各專案自己的規格。
#
# ── J8 的關鍵細節:只掃「卡片/站的本體」,其餘一概不掃 ──────────
# 踩過兩次同一類坑(第二次是本閘自己被真實文件抓包):
# ① 文件開頭的規矩說明裡寫「各 repo 的規格」→ 被自己的自檢抓到
# ② 文件結尾「這卷還缺什麼」的自述裡提到跨 repo 鏈路 → 又被抓到
# 兩者都不是卡片內容,是**文件在講自己**。
# 閘要是連這些都掃,人就只能把說明寫得不清不楚來換綠燈,本末倒置。
#
# 第一版用「排除法」(跳過 > 引言、註解、標題)——不夠,因為自述段是普通條列。
# 改用**正面圈定**:只掃真正的內容體
# · root.md `- **P<n>**` 卡片行 它底下的縮排子項
# · journeys.md `#### S<n>` 站標題以下、到下一個標題之前的內文
# 自述段、說明區、索引表因為不在這兩種範圍裡,自然就不會被掃到——
# 不必為它們一個個開例外。
#
# 誠實限制:只認字面與行首形狀。
# 「把技術概念用白話包裝起來」它看不出來(那要人讀);
# 用 bash 繞道改檔也擋不到。價值是擋掉明顯的格式錯誤與手滑,不是技術防偽。
set -uo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0
INPUT="$(cat)"
FILE_PATH="$(sdt_file_path "$INPUT")"
[ -z "$FILE_PATH" ] && exit 0
REL="$(sdt_rel_path "$FILE_PATH")"
CONTENT="$(sdt_write_content "$INPUT")"
[ -z "$CONTENT" ] && exit 0
TECH_TERMS='API|SDK|CLI|MCP|WASM|endpoint|schema|webhook|resturl|JSON|YAML|SQL|資料庫|後端|前端|部署|repo|commit|branch'
# 正面圈定要掃的行(見上方說明):
# root.md → `- **P<n>**` 卡片行 其縮排子項
# journeys.md → `#### S<n>` 站標題以下到下一個標題之前的內文
card_body_only() { # $1=root|journeys
if [ "$1" = "root" ]; then
awk '
/<!--/ { inc=1 } inc { if (/-->/) inc=0; next }
/^[[:space:]]*-[[:space:]]*\*\*P[0-9]+\*\*/ { incard=1; print NR "\t" $0; next }
incard && /^[[:space:]]+[-*]/ { print NR "\t" $0; next } # 卡片的縮排子項
{ incard=0 }
'
else
awk '
/<!--/ { inc=1 } inc { if (/-->/) inc=0; next }
/^####[[:space:]]*S[0-9]+/ { instation=1; next } # 進入某一站
/^#/ { instation=0; next } # 任何標題結束該站
instation && /^[[:space:]]*>/ { next } # 站內引言仍不掃
instation && /^[[:space:]]*$/ { next }
instation { print NR "\t" $0 }
'
fi
}
fail() { # $1=規則 $2=標題 $3=細節
cat >&2 <<EOF
❌ BLOCKED by jdd-format-guard(規則 $1
$2
檔案:$REL
$3
EOF
exit 2
}
# ── J4:root.md 的紅卡必須附【要驗證 + 對帳日】────────
if printf '%s' "$REL" | grep -q 'root\.md$'; then
# 逐張紅卡檢查:紅卡行之後、下一張卡之前,要出現「對帳日」
MISSING="$(printf '%s' "$CONTENT" | awk '
/^[[:space:]]*-[[:space:]]*\*\*P[0-9]+\*\*/ {
if (pending != "" && !found) { print pending }
found = 0
if ($0 ~ /🔴/) { pending = NR "\t" $0 } else { pending = "" }
next
}
pending != "" && /對帳日/ { found = 1 }
END { if (pending != "" && !found) print pending }
')"
if [ -n "$MISSING" ]; then
fail "J4" "紅卡(🔴)少了【要驗證 + 對帳日】。" \
" 下列紅卡沒有對帳行:
$(printf '%s' "$MISSING" | sed 's/^/ 行 /' | cut -c1-120)
補成這樣:
- **P9** 🔴 <一句白話>。
- 【要驗證:<能用真實數字或事實判真假的判準> | 對帳日 YYYY-MM-DD】
為什麼擋:紅卡是賭注。沒有判準和結算日的賭注,永遠不必認賠——
那不是賭注,是把「還沒做到」講得像「正在做」。"
fi
fi
# ── J8root.mdjourneys.md 不准出現技術名詞 ────────
case "$REL" in
*root.md|*journeys.md)
KIND="journeys"; printf '%s' "$REL" | grep -q 'root\.md$' && KIND="root"
HITS="$(printf '%s' "$CONTENT" | card_body_only "$KIND" | grep -inE "$TECH_TERMS" | head -8 || true)"
if [ -n "$HITS" ]; then
fail "J8" "PM 軌文件出現技術名詞。" \
" 命中(只掃卡片本體,已排除說明區/註解/標題):
$(printf '%s' "$HITS" | cut -c1-120 | sed 's/^/ /')
怎麼修:
· 把它翻成「使用者感覺得到的事」——例如不是「呼叫 API 取得資料」,
而是「我按下去之後,畫面上出現我的東西」
· 真的必須談技術 → 那句話屬於各專案自己的規格,不屬於這裡
為什麼擋:這兩份文件唯一的讀者是「不懂技術但要點頭或搖頭的人」。
出現一個他看不懂的詞,他就沒辦法判斷這張卡是不是他的意思。"
fi
;;
esac
# ── J5:tasks.md 新增的任務必須掛站號 ─────────────────
if printf '%s' "$REL" | grep -q 'tasks\.md$'; then
# 只看這次要寫入的內容裡「長得像新任務」的行
NEWTASKS="$(printf '%s' "$CONTENT" | grep -nE '^[[:space:]]*-[[:space:]]*\[[ x~!🔄]\][[:space:]]*[0-9]+\.[0-9]+' || true)"
if [ -n "$NEWTASKS" ]; then
# 站號標注:任務行本身或緊接的子項出現 S<n> / S-<n> / 「服務:S…」
NOSTATION="$(printf '%s' "$CONTENT" | awk '
/^[[:space:]]*-[[:space:]]*\[[ x~!🔄]\][[:space:]]*[0-9]+\.[0-9]+/ {
if (pending != "" && !found) print pending
pending = NR "\t" $0; found = 0
if ($0 ~ /S-?[0-9]+/) found = 1
next
}
pending != "" && /S-?[0-9]+/ { found = 1 }
/^[[:space:]]*$/ { if (pending != "" && !found) { print pending; pending=""; found=0 } }
END { if (pending != "" && !found) print pending }
')"
if [ -n "$NOSTATION" ]; then
fail "J5" "新增的任務沒有標注它服務哪一站。" \
" 下列任務缺站號:
$(printf '%s' "$NOSTATION" | cut -c1-120 | sed 's/^/ 行 /')
補成這樣:
- [ ] 1.1 <任務描述>
- 服務:S3(我按一次就裝到自己的地方)
- 驗收:<客觀可驗證的完成標準>
為什麼擋:sprint 的單位是站。任務不掛站,就沒有人答得出認領流程唯一的問題——
「這個任務不做,指定站的考題會掛嗎?」答不出來,這個任務就沒有理由在這一期做。"
fi
fi
fi
exit 0
@@ -0,0 +1,54 @@
#!/bin/bash
# regression-scope.sh — 動到某一站的東西 → 告訴你要重考哪些題(JDD 規則 J7)
#
# 掛 PostToolUseWrite|Edit|MultiEdit)。**只提醒,不阻擋**exit 0)。
#
# ── 為什麼只提醒 ────────────────────────────────
# 重考範圍是判斷題不是是非題——擋下來只會讓人為了繼續工作而亂填。
# 它要消滅的失敗模式只有一個:**改完東西,忘了它會弄壞哪幾站**。
# (「S3 安裝流程變了 → 後面 S4~S9 全部要重考」這種事,
# 不查索引表沒有人會自己想到。)
#
# ── 判斷「動到哪一站」怎麼做 ─────────────────────
# 靠 journeys.md 的**站點索引**。索引裡若有登記關聯路徑就用它;
# 沒有登記時退回一個誠實的作法:只要動到 code,就把索引整份摘要推一次,
# 讓人自己對——**不猜**。猜錯的重考範圍比沒有更危險(會讓人以為已經涵蓋了)。
set -uo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0
INPUT="$(cat)"
FILE_PATH="$(sdt_file_path "$INPUT")"
[ -z "$FILE_PATH" ] && exit 0
REL="$(sdt_rel_path "$FILE_PATH")"
# 只對「實作變動」出聲;改文件不必每次都喊
case "$REL" in
src/*|*/src/*|*.py|*.ts|*.tsx|*.js|*.go|*.rs|*.sh|*.java|*.rb|*.vue|*.svelte) ;;
*) exit 0 ;;
esac
ROOT="$(sdt_repo_root)"
JOURNEYS=""
for cand in "$ROOT/system-dev/docs/journeys.md" "$ROOT/../system-dev/docs/journeys.md"; do
[ -f "$cand" ] && JOURNEYS="$cand" && break
done
[ -z "$JOURNEYS" ] && exit 0 # 沒有 PM 軌文件(多數成員 repo)→ 安靜
# 取站點索引段(「附:站點索引」以下)
INDEX="$(awk '/^##[[:space:]]*附.*站點索引/{f=1} f' "$JOURNEYS" 2>/dev/null | head -60)"
[ -z "$INDEX" ] && exit 0
echo "════════════════════════════════════════════════"
echo "🔁 回歸考提醒(J7):你動了實作,查一下要重考哪幾題"
echo "════════════════════════════════════════════════"
echo " 剛動的:$REL"
echo ""
printf '%s\n' "$INDEX" | sed 's/^/ /'
echo ""
echo " ⚠️ 這份索引是「站 → 被哪些旅程經過 → 改動時重考什麼」。"
echo " 對照你剛改的東西屬於哪一站,把**下游**也一起重考——"
echo " 安裝那類的站一變,後面全部都要重來。"
exit 0
+153
View File
@@ -0,0 +1,153 @@
#!/bin/bash
# role-guard.sh — 角色封路:誰能寫什麼(JDD 規則 J1+J2+J3)
#
# 掛 PreToolUsematcher: Write|Edit|MultiEdit)。
#
# ── 為什麼三條規則合成一支 hook ─────────────────────────
# J1 orchestrator 不准寫 code
# J2 orchestrator 不准寫技術軌文件(任務池/規格)
# J3 engineer 不准改考卷(旅程/根文件/任何考題)
# 三條都是「同一個 hook 事件 × 同一份身分判定 × 同一張路徑表」。
# 拆三支=解析三次 JSON、三處維護同一張表,改一條規則要記得改三個檔。
# 規則編號保留在程式碼與錯誤訊息裡,追溯不會少。
#
# ── J3 是命門 ───────────────────────────────────────
# 考生不能改考卷。這條沒守住,整套 PM 軌就是裝飾品:
# 考題沒過的人,只要改一下考題就「過了」,而且沒有人會發現。
# 所以 J3 攔的不只是檔案,還包括 md 檔裡的 Gherkin 區塊——
# 考題常常就住在別的文件裡。
#
# 誠實限制:擋的是「直接寫檔」這個語法層動作。
# 用 bash 繞道(sed -i / cat > / python 改檔)擋不到。
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱不可能繞過。
set -uo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/role-lib.sh
. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0 # 函式庫不在就放行(容錯)
INPUT="$(cat)"
FILE_PATH="$(sdt_file_path "$INPUT")"
[ -z "$FILE_PATH" ] && exit 0 # 拿不到路徑 → 放行,寧可漏擋不誤殺
REL="$(sdt_rel_path "$FILE_PATH")"
ROLE="$(sdt_role)"
# 身分矩陣先驗(成員 repo × orchestrator 這格不存在)
sdt_assert_identity || exit 2
# ── 路徑分類 ───────────────────────────────────────
is_code_path() {
case "$1" in
src/*|*/src/*) return 0 ;;
*.py|*.ts|*.tsx|*.js|*.jsx|*.go|*.rs|*.java|*.rb|*.c|*.h|*.cpp|*.sh|*.bash|*.zsh) return 0 ;;
*.sql|*.vue|*.svelte|*.swift|*.kt|*.php) return 0 ;;
esac
return 1
}
is_tech_track_doc() { # 技術軌文件:任務池與規格
case "$1" in
*tasks.md|*requirements.md|*design.md) return 0 ;;
esac
return 1
}
is_exam_paper() { # 考卷:旅程/根文件/獨立考題檔
case "$1" in
*journeys.md|*root.md|*.feature) return 0 ;;
esac
return 1
}
# md 檔內含 Gherkin 區塊?(考題常寄住在別的文件裡)
content_has_gherkin() {
local c
c="$(sdt_write_content "$INPUT")"
[ -z "$c" ] && return 1
# ```gherkin 圍籬,或 journeys.md 的考題行式(- **G-1.1**),或 Given/When/Then 三件成組
printf '%s' "$c" | grep -qiE '```[[:space:]]*gherkin' && return 0
printf '%s' "$c" | grep -qE '^\s*-?\s*\*\*G-[0-9]+\.[0-9]+\*\*' && return 0
if printf '%s' "$c" | grep -qE '^\s*-?\s*Given ' \
&& printf '%s' "$c" | grep -qE '^\s*-?\s*When ' \
&& printf '%s' "$c" | grep -qE '^\s*-?\s*Then '; then
return 0
fi
return 1
}
block() { # $1=規則編號 $2=標題 $3=正確做法(多行)
cat >&2 <<EOF
❌ BLOCKED by role-guard(規則 $1
$2
身分:role=$ROLE(來源:AGENT_ROLE 環境變數/依安裝位置推定)
路徑:$REL
$3
EOF
exit 2
}
# ── J1J2orchestrator 的禁區 ─────────────────────
if [ "$ROLE" = "orchestrator" ]; then
if is_code_path "$REL"; then
block "J1" "總管(PM)不寫 code。" \
" 你的工作是決定「要點亮哪幾站、考題是什麼」,不是自己下去寫。
正確做法:
· 把它變成一個掛站號的任務,交給該 repo 的 engineer
· 你要的結果寫成考題(Gherkin),不是寫成實作
為什麼擋:PM 自己下去寫 code,就沒有人在看「零件之間有沒有人掉進縫裡」了
——那正是這個角色唯一不能被取代的價值。"
fi
if is_tech_track_doc "$REL"; then
block "J2" "總管(PM)不寫技術軌文件(任務池/規格)。" \
" 任務池與規格屬於 engineer。你對它們**只讀**。
正確做法:
· 要調整優先順序 → 改 sprint.md 的指定站,讓認領流程自己去挑任務
· 覺得少了任務 → 說出「哪一站的考題會掛」,由 engineer 認領或新增
· 要改驗收標準 → 改 journeys.md 的考題(那是你的檔)
為什麼擋:你一旦動手改任務清單,就等於繞過「先認領→不足才新增」的順序,
很快會長出第二份平行的任務池,兩邊不同步。"
fi
fi
# ── J3:engineer 不准改考卷(命門)─────────────────
if [ "$ROLE" = "engineer" ]; then
if is_exam_paper "$REL"; then
block "J3" "考生不能改考卷。" \
" journeys.mdroot.md/考題檔是 PM 的檔案,engineer 只能讀當期指定站的段落。
正確做法:
· 考題沒過 → 去把東西做對,不是把題目改掉
· 真的認為題目出錯了(判準不合理、站定義有誤)→ **回報 PM**,由他決定改不改
· 你需要的是「這站到底要什麼」→ 讀,不要寫
為什麼這條是命門:考題沒過的人只要改一下考題就「過了」,
而且沒有任何人會發現。這樣整套驗收就是裝飾品。"
fi
# md 檔本身不是考卷,但內容含考題 → 一樣擋(考題常寄住在別的文件裡)
case "$REL" in
*.md)
if content_has_gherkin; then
block "J3" "考生不能改考卷(這次寫入的內容含考題)。" \
" 這個檔名不是考卷,但你要寫進去的內容裡有 Gherkin 考題(GivenWhenThen 或 G-x.y)。
正確做法:
· 要記錄「我做到哪了」→ 寫 wiki/status,不要在文件裡複寫考題
· 要提出新考題 → 回報 PM,由他寫進 journeys.md
為什麼擋:考題散落成兩份就會不同步,
而不同步的那一刻起,「全綠」代表什麼就沒人說得準了。"
fi
;;
esac
fi
exit 0
@@ -26,6 +26,35 @@ if [ -d ".claude/wiki" ] && [ ! -f "$STATUS_FILE" ]; then
exit 0
fi
# ── profile 分流(1.19.0):總管起牀先看「還缺哪幾站」──────────
# 為什麼加這段:自動起牀的排程/新 session 最常見的失敗模式是
# 「醒來讀到一堆過期任務,只能空轉收工」。治它的不是更勤勞的提醒,
# 是**讓它一睜眼就有明確的『離通關還缺什麼』**——那就是本期指定站。
# 成員 repo 不需要這段(它的工作單位是任務不是站),維持原行為。
SDT_PROFILE="repo"
[ -f "system-dev/.profile" ] && SDT_PROFILE="$(tr -d '[:space:]' < system-dev/.profile 2>/dev/null || echo repo)"
if [ "$SDT_PROFILE" = "orchestrator" ] && [ -f "system-dev/docs/sprint.md" ]; then
echo "════════════════════════════════════════════════"
echo "🎯 本期要點亮哪幾站(進度語言=站,不是任務數)"
echo "════════════════════════════════════════════════"
# 期間、交付、現在幾站亮了——只取前 25 行,不整份灌
sed -n '/^## 本期/,/^## 認領流程/p' system-dev/docs/sprint.md 2>/dev/null \
| grep -vE '^## 認領流程' | head -25
echo ""
echo "📌 回報形式:「J-x 已點亮 n/m 站」。狀態只有 ✅ 通(附實測證據)/◐ 半通/❌ 斷。"
echo " 「程式碼寫完了」不是狀態。"
echo ""
if [ -f "system-dev/docs/root.md" ]; then
DUE="$(grep -oE '對帳日[^|】]*' system-dev/docs/root.md 2>/dev/null | head -3)"
if [ -n "$DUE" ]; then
echo "🔴 紅卡對帳日(到期要拿真實數據判決:承諾成立,或換一個承諾):"
printf '%s\n' "$DUE" | sed 's/^/ /'
echo ""
fi
fi
fi
# 三個 push 檔都沒有 → 安靜退出,不干擾還沒 /wiki-init 的專案
if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then
exit 0
@@ -0,0 +1,56 @@
#!/bin/bash
# station-done-guard.sh — 收工判準是「站的考題全綠」,不是「任務全關」(JDD 規則 J6)
#
# 掛 Stop / SubagentStop(收工那一刻),只在 orchestrator 實例生效。
#
# ── 它在攔什麼 ──────────────────────────────────
# 「任務都關了 ⇒ 這期做完了」——這句話是整套假綠的源頭。
# 任務是**技術軌**的單位(沿系統結構切),站是**PM 軌**的單位(沿人的經歷切)。
# 零件全部做完、每個都對,人還是可能掉進零件之間的縫裡。
# 所以收工只認一件事:**指定站的考題有沒有實測通過**。
#
# ── 它不做什麼(重要)────────────────────────────
# 它**不會**自己去跑考題判斷過沒過——Gherkin 的 Then 寫的是「使用者看到什麼」,
# 那本來就不是 shell 判得出來的。它做的是:在你要收工的那一刻,
# 把「指定站」和「它們現在的狀態」攤在你眼前,逼你面對還沒填的那幾格。
# 自動判綠反而危險:那會製造一個「機器說過了」的假權威。
#
# 誠實限制:讀的是 sprint.mdjourneys.md 的文字狀態。
# 有人亂填「✅」它看不出來——它擋的是「忘了填」與「用任務數充當進度」,不是造假。
set -uo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0
[ "$(sdt_scope)" = "orchestrator" ] || exit 0
ROOT="$(sdt_repo_root)"
SPRINT="$ROOT/system-dev/docs/sprint.md"
[ -f "$SPRINT" ] || exit 0 # 還沒開始用站號 sprint → 不干擾
# 本期指定的站
STATIONS="$(grep -oE 'S-?[0-9]+' "$SPRINT" 2>/dev/null | sort -u | tr '\n' ' ')"
[ -z "$STATIONS" ] && exit 0
# 還沒亮的站(sprint.md 的「❌ 未亮」「◐ 半通」段)
UNLIT="$(awk '/未亮|◐|半通/ {print}' "$SPRINT" 2>/dev/null | grep -oE 'S-?[0-9]+' | sort -u | tr '\n' ' ')"
echo "════════════════════════════════════════════════"
echo "🚦 收工判準:站的考題全綠,不是任務全關(J6)"
echo "════════════════════════════════════════════════"
echo " 本期指定站:${STATIONS}"
if [ -n "$UNLIT" ]; then
echo " ⚠️ 還沒亮:${UNLIT}"
echo ""
echo " 這幾站沒亮,這期就還沒收。收工前擇一:"
echo " · 真的通了 → 在 journeys.md 標記點亮,**附實測輸出**(不是「應該會過」)"
echo " · 半通 → 標 ◐ 並寫明缺什麼"
echo " · 沒通 → 標 ❌ 誠實留著,搬到下一期並寫一行為什麼"
echo ""
echo " ⛔ 不准用「任務都關了」當收工理由——那是技術軌的單位,不是驗收線。"
else
echo " ✅ 本期指定站都已標記點亮。記得每一站都要附得出實測證據。"
fi
echo ""
exit 0