Migration Protocol(schema 演进哲学)
May 5, 2026 · View on GitHub
被 cheat-migrate skill / cheat-init / SessionStart hook / 维护者引用。规定如何安全演进 .cheat-state.json schema 而不让老用户被打断。
核心原则
- 每个 release 必须能让老用户的旧 state 工作——通过 migrate 升级,或通过
state.get(field, default)兼容 - MINOR 改动 = 加字段 / 软化 enum;老 state 不跑 migrate 也能工作(字段缺失用默认值),跑了让 state 完整
- MAJOR 改动 = 删字段 / 重命名 / 改语义;老 state 必须跑 migrate,否则 skill 读到不一致字段会出错
- 不允许跳版;多版升级必须按顺序应用每个 step。每步幂等
- 失败停在原地;不回滚,让用户在断点修复后继续
- schema_version 是单调递增;不允许降级(如需降级,cp 历史 git 快照)
何时算 MINOR vs MAJOR
MINOR 范围(不需要 migrate 也能跑老 state)
- 新增字段(默认值定义良好,老 skill 不读它也不出错)
- 软化 enum 取值(如
"strict" / "lenient"加第三个"adaptive",老值仍合法) - 给字段加新可选取值(如某个 list 字段加新元素)
- 改默认值(不改语义)
MAJOR 范围(必须跑 migrate)
- 删除字段——老 skill 仍写它,新 skill 不读它,会出歧义
- 重命名字段——老/新 skill 看不到对方写的
- 改字段语义(如
mode从 enum 改为整数;baseline_plays从 int 改为 list) - 改 enum 取值(如
"opinion-video"改成"opinion_video",老值不再合法) - 拆字段 / 合字段
模糊地带建议保守判定为 MAJOR——多写一份 migration 文件比让用户的 state 出错更好。
维护者 checklist:bump schema 时必做的 4 件事
每次准备 release 时如果改了 state schema:
1. 改 cheat-init 写新 state 的硬编码 schema_version
- "schema_version": "1.1",
+ "schema_version": "1.2",
位置:skills/cheat-init/SKILL.md Phase 3 的 state 写入段。
2. 改 migrations/registry.md 的 LATEST_SCHEMA 标记位
- LATEST_SCHEMA = "1.1"
+ LATEST_SCHEMA = "1.2"
并在"版本链"表追加新行:
| 1.1 | 1.2 | NO/YES | [1.1-to-1.2.md](1.1-to-1.2.md) | 一句话描述 |
3. 写 migrations/-to-.md
4 段必填(参考 1.0-to-1.1.md 模板):
- WHAT changed
- WHY
- HOW (Claude steps for /cheat-migrate)
- Manual fallback
写不出 4 段 = 改动太复杂没想清楚 = 不该 release 这次 schema bump。
4. CHANGELOG.md 标版本号 + 链接
## [0.2.0] — YYYY-MM-DD
### BREAKING / MINOR
- schema_version 1.1 → 1.2: <一句话描述>。迁移指南:[migrations/1.1-to-1.2.md](migrations/1.1-to-1.2.md)
- ...
MINOR 用 ### MINOR,MAJOR 用 ### BREAKING,要醒目。
skill 内部怎么读 state(防御式编程)
每个 skill 读 state 时必须用 state.get(field, default) 模式:
# 好
target_cadence = state.get("target_publish_cadence_days", None)
benchmark_status = state.get("benchmark_status", "none")
shoots = state.get("shoots", [])
# 坏(老 state 没这字段会 KeyError)
target_cadence = state["target_publish_cadence_days"]
理由:
- MINOR 升级时老 state 缺新字段——
get模式让 skill 自动用默认值 - 用户手改 state 删了字段——同上
- 减少 skill 内"必须先迁移才能跑"的强依赖
例外:核心标识字段允许直接索引(如 state["schema_version"]、state["rubric_version"])——这些缺失意味着 state 文件根本不合法,应该明确报错。
SessionStart hook 的角色
hook 在每次会话开始时检测:
state_schema=$(jq -r '.schema_version // "unknown"' "$STATE_FILE")
if [[ "$state_schema" != "$LATEST_SCHEMA" ]]; then
echo "⚠️ schema 版本不一致:state=$state_schema, skill 期望=$LATEST_SCHEMA"
echo " 建议跑 /cheat-migrate 升级(不阻塞继续工作)"
fi
非阻塞:用户可以选择"先继续工作,回头再跑 migrate"。MINOR 不一致时大部分功能仍能跑;MAJOR 时部分 skill 可能报错——这时再跑 migrate 也来得及。
给开发者:避免 schema 频繁 bump 的实践
不是每个改动都需要 schema bump。下面是哲学:
- 优先 MINOR:能加字段就加字段,少删字段。删字段让老用户不爽
- 批量 bump:积攒 3-5 个 MINOR 一起 release 比每次小改都 bump 要友好
- 延迟 bump:MINOR 字段如果 90% 用户用不到,不急着 bump schema——可以让该字段
state.get(field, default)默默 work,等下次 release 顺路 bump - 避免 MAJOR:能用 MINOR 解决的绝不上 MAJOR。例:与其重命名字段,不如保留旧字段 + 加新字段(旧的标 deprecated,下个 MAJOR release 才删)
备份保留策略
/cheat-migrate 写之前会备份到 .cheat-state.json.backup-<timestamp>。
备份保留多久:
- 用户跑
/cheat-status时,如果有备份 + state 已稳定运行 N 天 → 提示"可以清理 N 个旧备份" /cheat-init重 init 时清理所有旧备份(既然要重 init,老备份意义不大)- 用户手动
rm .cheat-state.json.backup-*永远 OK
不入版本控制:.cheat-state.json.backup-* 应在 .gitignore 里(已含 .cheat-state.json 通配规则)。