feat(sdlc): 四階段收尾一律回報

What:新增 tools/stage-report.sh 與 references/stage-report.md,四支階段技能各加一步收尾回報:模型能力標籤判定、工作日誌連結、所有寫入的 wiki 連結;實作階段再加工作目錄與來源、工作、目標三條分支。

Why:階段跑完該交代什麼是固定的,寫在內文靠模型自己記,少一項看不出來。沒寫工作日誌更是如此——內容只留在對話裡,換一個工作階段就沒了。

How:彙整搬到程式層。頁名換絕對網址、commit 數與推送狀態由 git 現查、來源分支在不在遠端也由腳本判定;模型閘門那一列只轉述 sdlc-gate.sh report,不自評。沒有工作日誌就警告使用者檢查,並把內容交給 jsc-log 的暫存區,下次寫日誌一併寫入。結束碼 1 是警告不是阻擋,提前停下來也要回報。

Who:跑 SDLC 四階段的人,以及接手看紀錄的人。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:19:43 +08:00
co-authored by Claude Opus 5
parent 4a309a571d
commit 4a4feaf6ef
10 changed files with 350 additions and 11 deletions
+279
View File
@@ -0,0 +1,279 @@
#!/usr/bin/env sh
# stage-report.sh — SDLC 階段收尾回報(規劃、分析、實作、維護共用)。
#
# 為什麼要有這支腳本:階段結束要回報的東西是固定的——模型閘門判定、工作日誌連結、所有寫入的
# wiki 連結,實作階段再加上工作目錄與三條分支。寫在技能內文裡靠模型自己記,少一項看不出來;
# 所以彙整搬到程式層:頁名換成絕對網址、commit 數與推送狀態由 git 現查、沒有工作日誌就當場
# 警告並把內容存進暫存區,全部由本檔產出,技能只負責把事實餵進來。
#
# 用法:
# stage-report.sh <stage> [選項]
# <stage> = plan | analyze | implement | maintain
#
# 共用選項:
# --page TYPE:PAGE 本階段寫入的 wiki 頁,可重複(例:--page ANALYZE:ANALYZE_D3F1A2B0)
# --worklog PAGE 已寫入的工作日誌頁名(例:LOG_D3F1A2B0)
# --worklog-heading TEXT 日誌條目標題,用來組出導向該條目的錨點連結
# --pending-file FILE 沒有工作日誌時,要暫存起來的日誌內容檔
# --log-hash HASH 暫存歸屬的工作日誌 hash(即 LOG_{HASH} 的 HASH)
#
# 實作階段選項:
# --worktree PATH 工作目錄(worktree 路徑)
# --source-branch NAME 來源分支(本檔自行判定遠端有沒有這一條)
# --work-branch NAME 工作分支(本檔自行算 commit 數與推送狀態)
# --pr URL 目標分支的 PR 連結
# --target-branch NAME 目標分支(省略時等同來源分支)
#
# 輸出: 繁體中文 markdown 階段回報,直接貼給使用者。
# 結束碼: 0=回報完整 1=有警告(缺工作日誌,內容已暫存) 2=用法錯誤 3=相依工具找不到
#
# 陷阱:
# - 結束碼 1 是警告,不是阻擋:階段的工作已經做完了,擋下去只會讓使用者拿不到回報。
# - 缺工作日誌時一定要給 --pending-file 與 --log-hash,否則內容留在對話裡,換一個工作階段就沒了。
# - 模型閘門那一列只轉述 sdlc-gate.sh report 的結果,不自己判定標籤。
set -u
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}"
usage() {
cat >&2 <<'EOF'
用法:
stage-report.sh <plan|analyze|implement|maintain> [選項]
--page TYPE:PAGE 本階段寫入的 wiki 頁,可重複
--worklog PAGE 已寫入的工作日誌頁名
--worklog-heading TEXT 日誌條目標題(組錨點用)
--pending-file FILE 沒有工作日誌時要暫存的內容檔
--log-hash HASH 暫存歸屬的工作日誌 hash
--worktree PATH 實作:工作目錄
--source-branch NAME 實作:來源分支
--work-branch NAME 實作:工作分支
--target-branch NAME 實作:目標分支(省略時同來源分支)
--pr URL 實作:PR 連結
結束碼: 0=完整 1=缺工作日誌(已暫存,警告) 2=用法錯誤 3=相依工具找不到
EOF
exit 2
}
# 找相依腳本。兩種版面都要顧到:並排存取庫(開發用)與已安裝 plugin(各版本一個目錄)。
resolve_dep() { # $1=並排目錄名(gitea、hooks、log) $2=plugin 內的相對路徑
_name="$1"; _rel="$2"
for _c in "$plugin_root/../$_name/$_rel" "$plugin_root/../jsc-$_name/$_rel"; do
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
done
_c=$(ls -d "$plugin_root"/../../"jsc-$_name"/*/"$_rel" \
"$plugin_root"/../../"$_name"/*/"$_rel" \
"$HOME"/.claude/plugins/cache/*/"jsc-$_name"/*/"$_rel" 2>/dev/null \
| sort | tail -n1)
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
return 1
}
gitea_sh() {
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
fi
resolve_dep gitea tools/gitea.sh
}
sdlc_gate_sh() {
if [ -n "${JSC_HOOKS_DIR:-}" ] && [ -f "$JSC_HOOKS_DIR/sdlc-gate.sh" ]; then
printf '%s\n' "$JSC_HOOKS_DIR/sdlc-gate.sh"; return 0
fi
resolve_dep hooks hooks/sdlc-gate.sh
}
stage="${1:-}"
case "$stage" in
plan|analyze|implement|maintain) shift ;;
*) usage ;;
esac
pages=''
worklog=''
worklog_heading=''
pending_file=''
log_hash=''
worktree=''
source_branch=''
work_branch=''
target_branch=''
pr_url=''
while [ "$#" -gt 0 ]; do
case "$1" in
--page) [ "$#" -ge 2 ] || usage; pages="$pages$2
"; shift 2 ;;
--worklog) [ "$#" -ge 2 ] || usage; worklog="$2"; shift 2 ;;
--worklog-heading) [ "$#" -ge 2 ] || usage; worklog_heading="$2"; shift 2 ;;
--pending-file) [ "$#" -ge 2 ] || usage; pending_file="$2"; shift 2 ;;
--log-hash) [ "$#" -ge 2 ] || usage; log_hash="$2"; shift 2 ;;
--worktree) [ "$#" -ge 2 ] || usage; worktree="$2"; shift 2 ;;
--source-branch) [ "$#" -ge 2 ] || usage; source_branch="$2"; shift 2 ;;
--work-branch) [ "$#" -ge 2 ] || usage; work_branch="$2"; shift 2 ;;
--target-branch) [ "$#" -ge 2 ] || usage; target_branch="$2"; shift 2 ;;
--pr) [ "$#" -ge 2 ] || usage; pr_url="$2"; shift 2 ;;
*) echo "[jsc][階段回報][ERR]:不認得的選項「$1」。" >&2; usage ;;
esac
done
GITEA=$(gitea_sh) || { echo '[jsc][階段回報][ERR]:找不到 jsc-gitea/tools/gitea.sh,無法把頁名換成網址。' >&2; exit 3; }
GATE=$(sdlc_gate_sh) || { echo '[jsc][階段回報][ERR]:找不到 jsc-hooks/hooks/sdlc-gate.sh,無法轉述閘門判定。' >&2; exit 3; }
stage_zh() {
case "$1" in
plan) echo '規劃' ;;
analyze) echo '分析' ;;
implement) echo '實作' ;;
maintain) echo '維護' ;;
esac
}
required_tag() {
case "$1" in
plan|analyze) echo 'reasoning-max' ;;
implement) echo 'coding' ;;
maintain) echo '無(模型 id 可判定即通過)' ;;
esac
}
# 錨點:Gitea wiki 的標題錨點比照 GitHub——轉小寫、空白換連字號、去掉標點;中文原樣保留。
anchor_of() {
printf '%s' "$1" \
| tr 'A-Z' 'a-z' \
| sed 's/[][()#。,、:;!?,.:;!?]//g; s/[[:space:]]\{1,\}/-/g'
}
page_url() { # TYPE PAGE -> 絕對網址;查不到就回空字串
_type="$1"; _page="$2"
_repo=$("$GITEA" wiki-repo "$_type" 2>/dev/null) || return 1
"$GITEA" wiki-url "$_repo" "$_page" 2>/dev/null || return 1
}
# ---- 模型能力標籤 ----
gate_line=$("$GATE" report 2>/dev/null || true)
gate_stage=$(printf '%s' "$gate_line" | awk '{print $2}')
gate_tag=$(printf '%s' "$gate_line" | awk '{print $3}')
gate_model=$(printf '%s' "$gate_line" | awk '{print $4}')
if [ -z "$gate_line" ]; then
gate_result="查不到閘門狀態(未上鎖或狀態檔不在),請重跑 sdlc-gate.sh lock $stage"
elif [ "$gate_stage" != "$stage" ]; then
gate_result="不符:本階段為 $stage,鎖上的卻是 $gate_stage,請重跑 sdlc-gate.sh lock $stage"
else
gate_result="通過(必要標籤 $(required_tag "$stage"),實際模型 ${gate_model:-未知},鎖上時的標籤 ${gate_tag:-無})"
fi
# ---- 工作日誌 ----
warn=0
worklog_cell=''
if [ -n "$worklog" ]; then
wl_url=$(page_url LOG "$worklog" || true)
if [ -n "$wl_url" ]; then
if [ -n "$worklog_heading" ]; then
worklog_cell="[$worklog_heading]($wl_url#$(anchor_of "$worklog_heading"))"
else
worklog_cell="[$worklog]($wl_url)"
fi
else
worklog_cell="$worklog(取不到網址,請確認 LOG 的 wiki 存取庫設定)"
fi
else
warn=1
worklog_cell='**尚未寫入**'
fi
# ---- 暫存 ----
pending_note=''
if [ "$warn" -eq 1 ]; then
if [ -n "$pending_file" ] && [ -n "$log_hash" ]; then
PENDING=$(resolve_dep log tools/worklog-pending.sh || true)
if [ -n "$PENDING" ]; then
if saved=$("$PENDING" add "$log_hash" "$pending_file" 2>&1); then
pending_note="已暫存到 $saved,下次 jsc-log:worklog 寫入時一併寫進去。"
else
pending_note="暫存失敗:$saved"
fi
else
pending_note='找不到 jsc-log/tools/worklog-pending.sh,這次的內容沒有暫存起來。'
fi
else
pending_note='沒有給 --pending-file 與 --log-hash,這次的內容沒有暫存起來。'
fi
fi
# ---- 分支資訊(實作階段) ----
branch_rows=''
if [ "$stage" = implement ]; then
wt="${worktree:-}"
git_in() { # 在工作目錄裡跑 git
if [ -n "$wt" ]; then git -C "$wt" "$@" 2>/dev/null; else git "$@" 2>/dev/null; fi
}
src_cell='未提供'
if [ -n "$source_branch" ]; then
if git_in show-ref --verify --quiet "refs/remotes/origin/$source_branch"; then
src_cell="origin/$source_branch(遠端)"
else
src_cell="$source_branch(**遠端找不到**,只有本機)"
fi
fi
work_cell='未提供'
if [ -n "$work_branch" ]; then
base="origin/$source_branch"
git_in show-ref --verify --quiet "refs/remotes/origin/$source_branch" || base="$source_branch"
count=$(git_in rev-list --count "$base..$work_branch" || true)
[ -n "$count" ] || count='?'
if git_in show-ref --verify --quiet "refs/remotes/origin/$work_branch"; then
ahead=$(git_in rev-list --count "origin/$work_branch..$work_branch" || true)
if [ "${ahead:-0}" = 0 ]; then push_cell='已 push'; else push_cell="尚有 ${ahead} 個 commit 未 push"; fi
else
push_cell='**未 push**'
fi
work_cell="$work_branch($count commit、$push_cell)"
fi
tgt="${target_branch:-$source_branch}"
if [ -n "$pr_url" ]; then
tgt_cell="${tgt:-未提供}(PR $pr_url)"
else
tgt_cell="${tgt:-未提供}(**尚無 PR**)"
warn=1
fi
branch_rows=$(cat <<EOF
| 工作目錄 | ${worktree:-未提供} |
| 來源分支 | $src_cell |
| 工作分支 | $work_cell |
| 目標分支 | $tgt_cell |
EOF
)
fi
# ---- 輸出 ----
printf '## 階段回報:%s\n\n' "$(stage_zh "$stage")"
printf '| 項目 | 內容 |\n| --- | --- |\n'
printf '| 模型能力標籤 | %s |\n' "$gate_result"
printf '| 工作日誌 | %s |\n' "$worklog_cell"
[ -n "$branch_rows" ] && printf '%s\n' "$branch_rows"
printf '\n### 寫入的 wiki 頁\n\n'
if [ -n "$pages" ]; then
printf '| 頁面 | 連結 |\n| --- | --- |\n'
printf '%s' "$pages" | while IFS= read -r item; do
[ -n "$item" ] || continue
type=${item%%:*}
page=${item#*:}
url=$(page_url "$type" "$page" || true)
printf '| %s | %s |\n' "$page" "${url:-(取不到網址)}"
done
else
printf '本階段沒有寫入任何 wiki 頁。\n'
fi
if [ "$warn" -eq 1 ]; then
printf '\n### 警告\n\n'
[ -n "$worklog" ] || printf -- '- 這個階段還沒有寫入工作日誌,請自行檢查是不是漏了。%s\n' "$pending_note"
[ "$stage" = implement ] && [ -z "$pr_url" ] && printf -- '- 目標分支還沒有 PR,工作尚未交出去。\n'
exit 1
fi
exit 0