219 lines
8.9 KiB
Bash
Executable File
219 lines
8.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# cheat-on-content SessionStart hook
|
||
#
|
||
# Renders a 4-6 line status report at the start of every Claude Code session.
|
||
# Output is added to Claude's system context — Claude sees it before first reply.
|
||
#
|
||
# Silently exits if:
|
||
# - Not in a cheat-on-content project (no .cheat-state.json)
|
||
# - jq not available (status is markdown-readable; Claude can read state.json directly)
|
||
#
|
||
# Format:
|
||
# 📦 Buffer: N (color)
|
||
# ⏰ 待复盘: N
|
||
# 🎯 候选 top 3: ...
|
||
# 📅 上次抓热点: N 天前
|
||
# ⚠️ 待办: ...
|
||
|
||
set -uo pipefail
|
||
|
||
# Portable ISO-8601 timestamp → epoch converter (works on both GNU/Linux and BSD/macOS)
|
||
parse_iso_epoch() {
|
||
local input="${1%%+*}" # strip timezone suffix like +08:00
|
||
input="${input%%Z}" # strip Z suffix
|
||
date -d "$input" "+%s" 2>/dev/null \
|
||
|| date -j -f "%Y-%m-%dT%H:%M:%S" "$input" "+%s" 2>/dev/null \
|
||
|| echo 0
|
||
}
|
||
|
||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
||
STATE_FILE="$PROJECT_DIR/.cheat-state.json"
|
||
|
||
# Silently skip if not a cheat-on-content project
|
||
if [[ ! -f "$STATE_FILE" ]]; then
|
||
exit 0
|
||
fi
|
||
|
||
# Skip if jq missing (Claude can still read state.json himself in conversation)
|
||
if ! command -v jq >/dev/null 2>&1; then
|
||
cat <<'EOF'
|
||
[cheat-on-content] SessionStart: jq not installed — skipping auto status report.
|
||
Claude can still read .cheat-state.json directly. Say "状态" for full status.
|
||
EOF
|
||
exit 0
|
||
fi
|
||
|
||
now_epoch=$(date +%s)
|
||
today_iso=$(date +%Y-%m-%d)
|
||
|
||
# --- Read state ---
|
||
state=$(cat "$STATE_FILE")
|
||
schema_version=$(echo "$state" | jq -r '.schema_version // "unknown"')
|
||
rubric_version=$(echo "$state" | jq -r '.rubric_version // "v0"')
|
||
calibration_samples=$(echo "$state" | jq -r '.calibration_samples // 0')
|
||
target_cadence=$(echo "$state" | jq -r '.target_publish_cadence_days // null')
|
||
buffer_count=$(echo "$state" | jq -r '.shoots // [] | length')
|
||
pending_retros_count=$(echo "$state" | jq -r '.pending_retros // [] | length')
|
||
last_trends_at=$(echo "$state" | jq -r '.last_trends_run_at // ""')
|
||
last_published_at=$(echo "$state" | jq -r '.last_published_at // ""')
|
||
hooks_installed=$(echo "$state" | jq -r '.hooks_installed // false')
|
||
form_severe_mismatch=$(echo "$state" | jq -r '.rubric_form_severe_mismatch // false')
|
||
last_prediction_self_scored=$(echo "$state" | jq -r '.last_prediction_self_scored // false')
|
||
last_self_scored_at=$(echo "$state" | jq -r '.last_self_scored_at // ""')
|
||
|
||
# --- Detect schema mismatch (read LATEST_SCHEMA from migrations/registry.md if reachable) ---
|
||
# Strategy: hardcode current LATEST_SCHEMA here (bumped by maintainer alongside cheat-init).
|
||
# If state.schema_version != LATEST_SCHEMA → suggest migrate (non-blocking).
|
||
LATEST_SCHEMA="1.4"
|
||
schema_mismatch=""
|
||
if [[ "$schema_version" != "$LATEST_SCHEMA" && "$schema_version" != "unknown" ]]; then
|
||
schema_mismatch="⚠️ schema 版本不一致:state=${schema_version}, skill 期望=${LATEST_SCHEMA}。建议跑 /cheat-migrate(非阻塞,部分新功能可能在迁移前异常)。"
|
||
elif [[ "$schema_version" == "unknown" ]]; then
|
||
schema_mismatch="⚠️ state.schema_version 字段缺失或损坏。建议跑 /cheat-status 检查文件,或备份后重 init。"
|
||
fi
|
||
|
||
# --- Detect blind-skip contamination (cheat-predict --skip-blind 或 Phase 2.5 选 b 触发) ---
|
||
self_scored_warning=""
|
||
if [[ "$last_prediction_self_scored" == "true" && -n "$last_self_scored_at" ]]; then
|
||
# Parse timestamp; tolerate +08:00 or Z suffix
|
||
self_scored_epoch=$(parse_iso_epoch "$last_self_scored_at")
|
||
if [[ $self_scored_epoch -gt 0 ]]; then
|
||
days_since=$(( (now_epoch - self_scored_epoch) / 86400 ))
|
||
if [[ $days_since -ge 7 ]]; then
|
||
self_scored_warning="🚨 距上次 \`--skip-blind\` 自评预测已 ${days_since} 天——校准池累计的 contamination 风险在叠加。下次 /cheat-predict 走 sub-agent 即可清除此提示。"
|
||
else
|
||
self_scored_warning="⚠️ 上次预测走了 \`--skip-blind\`(${days_since} 天前自评,未经 channel B 隔离)。下次 /cheat-predict 走默认即可清除。"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# --- Derive confidence label (single source: state-management.md confidence 表) ---
|
||
if [[ $calibration_samples -eq 0 ]]; then
|
||
confidence="🔴 极低 (占星级别,纯纪律训练)"
|
||
elif [[ $calibration_samples -le 2 ]]; then
|
||
confidence="🟠 低 (中枢 ±50%,方向感优于绝对数字)"
|
||
elif [[ $calibration_samples -le 5 ]]; then
|
||
confidence="🟡 偏低 (中枢 ±40%,可作为参考之一)"
|
||
elif [[ $calibration_samples -le 10 ]]; then
|
||
confidence="🟢 中 (中枢 ±25%,可参与决策)"
|
||
elif [[ $calibration_samples -le 20 ]]; then
|
||
confidence="🟢 较高 (中枢 ±15%,rubric 形态稳定)"
|
||
else
|
||
confidence="🔵 高 (中枢 ±10%,可数据驱动)"
|
||
fi
|
||
|
||
# --- Compute buffer color ---
|
||
buffer_label=""
|
||
buffer_warning=""
|
||
if [[ "$target_cadence" == "null" ]] || [[ -z "$target_cadence" ]]; then
|
||
# Flexible cadence: no color, just count
|
||
buffer_label="📦 Buffer: ${buffer_count} 篇 (灵活节奏,无警戒)"
|
||
else
|
||
target_cadence_int=${target_cadence%%.*}
|
||
buffer_days=$(( buffer_count * target_cadence_int ))
|
||
if [[ $buffer_days -lt 1 ]]; then
|
||
buffer_label="📦 Buffer: ${buffer_count} 篇 🔴 红 (按 cadence ${target_cadence}d = <1 天预备)"
|
||
buffer_warning="🚨 buffer 警戒:下个发布日可能断更。今天必须拍 ≥1 条稳分。"
|
||
elif [[ $buffer_days -le 2 ]]; then
|
||
buffer_label="📦 Buffer: ${buffer_count} 篇 🟠 橙 (按 cadence ${target_cadence}d = ${buffer_days} 天预备)"
|
||
elif [[ $buffer_days -le 5 ]]; then
|
||
buffer_label="📦 Buffer: ${buffer_count} 篇 🟢 绿 (按 cadence ${target_cadence}d = ${buffer_days} 天预备)"
|
||
else
|
||
buffer_label="📦 Buffer: ${buffer_count} 篇 🔵 蓝 (按 cadence ${target_cadence}d = ${buffer_days} 天,积压)"
|
||
buffer_warning="📦 buffer 积压:建议暂停拍摄,先发存货 + 复盘。"
|
||
fi
|
||
fi
|
||
|
||
# --- Compute pending retros that are actually due ---
|
||
retro_window=3 # default RETRO_WINDOW_DAYS, hardcoded fallback (TODO: read from rubric_notes if present)
|
||
due_count=0
|
||
earliest_due=""
|
||
if [[ "$pending_retros_count" -gt 0 ]]; then
|
||
# Walk pending_retros, check each prediction file's published_at
|
||
while IFS= read -r pred_file; do
|
||
pred_path="$PROJECT_DIR/$pred_file"
|
||
if [[ -f "$pred_path" ]]; then
|
||
pub_iso=$(grep -E '^\*\*Published at\*\*:' "$pred_path" 2>/dev/null | head -1 | sed -E 's/.*: *//')
|
||
if [[ -n "$pub_iso" ]]; then
|
||
pub_epoch=$(parse_iso_epoch "$pub_iso")
|
||
if [[ $pub_epoch -gt 0 ]]; then
|
||
age_days=$(( (now_epoch - pub_epoch) / 86400 ))
|
||
if [[ $age_days -ge $retro_window ]]; then
|
||
due_count=$((due_count + 1))
|
||
if [[ -z "$earliest_due" ]] || [[ "$pub_iso" < "$earliest_due" ]]; then
|
||
earliest_due="$pub_iso"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
done < <(echo "$state" | jq -r '.pending_retros // [] | .[]')
|
||
fi
|
||
|
||
retro_label=""
|
||
if [[ $due_count -gt 0 ]]; then
|
||
retro_label="⏰ 待复盘: ${due_count} 篇 (最早: ${earliest_due%%T*})"
|
||
elif [[ "$pending_retros_count" -gt 0 ]]; then
|
||
retro_label="⏰ 待复盘: ${pending_retros_count} 篇 (未到 T+${retro_window}d)"
|
||
else
|
||
retro_label="⏰ 待复盘: 无"
|
||
fi
|
||
|
||
# --- Top candidates (read first 3 H3 from candidates.md) ---
|
||
candidates_file="$PROJECT_DIR/candidates.md"
|
||
top_candidates=""
|
||
if [[ -f "$candidates_file" ]]; then
|
||
# Extract first 3 H3 titles, format compactly
|
||
top_candidates=$(grep -E '^### ' "$candidates_file" 2>/dev/null \
|
||
| head -3 \
|
||
| sed -E 's/^### \[[^]]+\] *//' \
|
||
| tr '\n' '/' \
|
||
| sed 's:/$::' \
|
||
| sed 's:/: / :g')
|
||
fi
|
||
if [[ -z "$top_candidates" ]]; then
|
||
candidates_label="🎯 候选: (空——说 '抓热点' 或 '找选题')"
|
||
else
|
||
candidates_label="🎯 候选 top 3: ${top_candidates}"
|
||
fi
|
||
|
||
# --- Last trends run ---
|
||
trends_label=""
|
||
if [[ -n "$last_trends_at" ]]; then
|
||
trends_epoch=$(parse_iso_epoch "$last_trends_at")
|
||
if [[ $trends_epoch -gt 0 ]]; then
|
||
days_ago=$(( (now_epoch - trends_epoch) / 86400 ))
|
||
trends_label="📅 上次抓热点: ${days_ago} 天前"
|
||
fi
|
||
fi
|
||
|
||
# --- Build the report ---
|
||
echo ""
|
||
echo "[cheat-on-content / SessionStart 状态报告]"
|
||
echo ""
|
||
echo "$buffer_label"
|
||
echo "$retro_label"
|
||
echo "$candidates_label"
|
||
[[ -n "$trends_label" ]] && echo "$trends_label"
|
||
|
||
# Confidence indicator
|
||
echo "📈 校准样本: ${calibration_samples} | Confidence: ${confidence}"
|
||
|
||
# Warnings (high priority)
|
||
[[ -n "$buffer_warning" ]] && echo "" && echo "$buffer_warning"
|
||
[[ -n "$schema_mismatch" ]] && echo "" && echo "$schema_mismatch"
|
||
[[ -n "$self_scored_warning" ]] && echo "" && echo "$self_scored_warning"
|
||
if [[ "$form_severe_mismatch" == "true" ]]; then
|
||
echo "❌ rubric 与你的内容形态严重不匹配——预测几乎无意义。"
|
||
fi
|
||
if [[ "$hooks_installed" != "true" ]]; then
|
||
echo "⚠️ immutability hook 未装——你的盲预测保护是君子协定,不是物理强制。"
|
||
fi
|
||
|
||
echo ""
|
||
echo "(不要主动开始任何动作——等用户决定。说 \"状态\" 看完整看板。)"
|
||
echo ""
|
||
|
||
exit 0
|