#!/usr/bin/env sh # report-status.sh — 技能與 hook 的執行狀態事件流。 # # 為什麼要有這支:現行 usage/skills.jsonl 只記「被叫用」,欄位是 {ts,cli,session,skill}, # 沒有成敗、沒有結束碼。跑完整輪的技能與開場就中止的技能,在紀錄裡長得一模一樣。 # hook 成功時更是完全不留紀錄,只有錯誤路徑會寫 wiki,而那條路徑刻意不自動觸發。 # # 為什麼不直接寫 wiki:hook 每次提示都跑,網路寫入會拖垮宿主 CLI;失敗的 hook 自我回報 # 還會疊出迴圈。所以一律先寫本機事件流,助理巡檢時排空、彙整、寫 MONITOR 頁。 # # 用法: # report-status.sh skill-start <名稱> # report-status.sh skill-end <名稱> [結束碼] [detail] # report-status.sh hook-end <名稱> <結束碼> [detail] # report-status.sh drain # 印出上次排空之後的新事件 # report-status.sh rotate # 超過上限就輪替,只留一份舊的 # # <名稱>: 技能寫 {domain}:{skill},hook 寫 {腳本檔名} 加子命令,例如 sdlc-gate check。 # : ok、blocked、failed、degraded、aborted 五選一。 # ok 完成條件全部達成 # blocked 被閘門或前置條件擋下,沒有做事 # failed 做到一半失敗 # degraded 做完了但有部分沒達成 # aborted 使用者中止,或前提不成立而主動停止 # # 規則: # - 三個記錄子命令一律回 0,寫檔失敗也是 0。回報機制自己壞掉,不可以讓被回報的東西 # 跟著壞——hook 的結束碼是閘門的判準,被記錄動到就等於閘門行為被記錄改寫。 # 參數檢查是例外:那是呼叫端的程式錯誤,寫進去只會汙染事件流,所以先擋下來。 # - 本檔不讀 stdin。技能由 Bash 呼叫它,stdin 可能是還沒關閉的管線,讀下去會卡住宿主。 # 所有資訊一律走參數。 # - 輪替不放在每次寫入。每次提示都寫事件,順手 stat 一次檔案就是每次提示多一次系統呼叫; # 改由巡檢排空之後呼叫 rotate,成本落在本來就週期性執行的地方。 # # 結束碼: 0=成功(記錄子命令一律 0) 2=用法錯誤 3=drain 沒有新事件 set -eu HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) # lib.sh 在同一個存取庫,不是跨 plugin 依賴。hook 必須自足,事件寫入的函式因此留在 lib.sh, # 這支只是把它包成命令列介面給技能用。 STDIN_JSON="" . "$HERE/../hooks/lib.sh" EVENTS="$JSC_HOME/usage/events.jsonl" OFFSET="$JSC_HOME/usage/scan-state/events.offset" MAX_BYTES=5242880 usage() { cat >&2 <<'EOF' 用法: report-status.sh skill-start <名稱> report-status.sh skill-end <名稱> [結束碼] [detail] report-status.sh hook-end <名稱> <結束碼> [detail] report-status.sh drain report-status.sh rotate status: ok、blocked、failed、degraded、aborted 結束碼: 0=成功 2=用法錯誤 3=drain 沒有新事件 EOF exit 2 } valid_status() { case "$1" in ok|blocked|failed|degraded|aborted) ;; *) echo "[jsc][狀態回報][ERR]:status 須為 ok、blocked、failed、degraded、aborted 五選一,收到「$1」。" >&2; exit 2 ;; esac } valid_exit() { case "$1" in ''|*[!0-9]*) echo "[jsc][狀態回報][ERR]:結束碼須為非負整數,收到「$1」。" >&2; exit 2 ;; esac } cmd="${1:-}"; [ -n "$cmd" ] || usage shift || true case "$cmd" in skill-start) name="${1:-}"; [ -n "$name" ] || usage # start 沒有成敗可言,狀態欄固定 ok、結束碼固定 0。判讀靠的是「有沒有配對的 end」: # 有 start 沒 end 就是中止,那正是現行紀錄分不出來的那一種。 emit_event skill "$name" start ok 0 ;; skill-end) name="${1:-}"; status="${2:-}" [ -n "$name" ] && [ -n "$status" ] || usage valid_status "$status" code="${3:-0}"; valid_exit "$code" emit_event skill "$name" end "$status" "$code" "" "${4:-}" ;; hook-end) name="${1:-}"; status="${2:-}"; code="${3:-}" [ -n "$name" ] && [ -n "$status" ] && [ -n "$code" ] || usage valid_status "$status"; valid_exit "$code" emit_event hook "$name" end "$status" "$code" "" "${4:-}" ;; drain) [ -f "$EVENTS" ] || exit 3 size=$(wc -c < "$EVENTS" 2>/dev/null || echo 0) old=0 [ -f "$OFFSET" ] && old=$(cat "$OFFSET" 2>/dev/null || echo 0) case "$old" in ''|*[!0-9]*) old=0 ;; esac # 檔案比已存位移還小就是輪替過,從頭讀。不比對 inode:五支 CLI 與容器裡的行程 # 看到的 inode 不保證一致,用大小判斷才在每個環境都成立。 [ "$size" -lt "$old" ] && old=0 [ "$size" -eq "$old" ] && exit 3 mkdir -p "$(dirname "$OFFSET")" 2>/dev/null || true # tail -c +N 從第 N 個位元組起(1 起算),所以位移要加一。 # 不用 dd bs=1 skip=:那是一個位元組一次系統呼叫,位移到了幾 MB 就是幾百萬次, # 每輪巡檢都排空一次的話會慢到不能用。 tail -c "+$((old + 1))" "$EVENTS" 2>/dev/null || true printf '%s' "$size" > "$OFFSET" 2>/dev/null || true ;; rotate) [ -f "$EVENTS" ] || exit 0 size=$(wc -c < "$EVENTS" 2>/dev/null || echo 0) if [ "$size" -gt "$MAX_BYTES" ]; then mv "$EVENTS" "$EVENTS.1" 2>/dev/null || true : > "$EVENTS" 2>/dev/null || true # 位移歸零:新檔從頭算起,不歸零的話下一次 drain 會跳過開頭那一段。 printf '0' > "$OFFSET" 2>/dev/null || true printf '已輪替:%s -> %s.1(原大小 %s 位元組)\n' "$EVENTS" "$EVENTS" "$size" fi ;; *) usage ;; esac exit 0