#!/usr/bin/env sh # stage-report.sh — SDLC 階段收尾回報(規劃、分析、實作、維護共用)。 # # 為什麼要有這支腳本:階段結束要回報的東西是固定的——模型閘門判定、工作日誌連結、所有寫入的 # wiki 連結,實作階段再加上工作目錄與三條分支。寫在技能內文裡靠模型自己記,少一項看不出來; # 所以彙整搬到程式層:頁名換成絕對網址、commit 數與推送狀態由 git 現查、沒有工作日誌就當場 # 警告並把內容存進暫存區,全部由本檔產出,技能只負責把事實餵進來。 # # 用法: # stage-report.sh [選項] # = 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 [選項] --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 <