Files
2026-07-13 12:29:17 +08:00

219 lines
8.9 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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