282 lines
11 KiB
Bash
Executable File
282 lines
11 KiB
Bash
Executable File
#!/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}')
|
||
gate_source=$(printf '%s' "$gate_line" | awk '{print $5}')
|
||
gate_verdict=$(printf '%s' "$gate_line" | awk '{print $6}')
|
||
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_source:-未知},判定 ${gate_verdict:-未知},鎖上時的標籤 ${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
|