第 20 章 | 基于文件的状态机
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)
(申请发送: agentupdate)
第 20 章:基于文件的状态机
学习目标
理解为什么状态要存在文件里而不是 main Claude 的"记忆"里——这是系统从"玩具"变"生产级"的分水岭。
内存状态 vs 文件状态
flowchart LR
subgraph Bad["内存状态(坏)"]
M1["main Claude 记得
'组 3 测试通过了'"]
end
subgraph Good["文件状态(好)"]
F1["test-reports/3.md
**Status:** PASS"]
end
M1 -->|/clear / 崩溃| Lost["状态丢失"]
F1 -->|/clear / 崩溃| Recover["重读文件即恢复"]
style Bad fill:#ffcdd2
style Good fill:#c8e6c9→ 文件状态 = crash-safe + 可审计 + 可恢复。
完整状态机
stateDiagram-v2
[*] --> PENDING
PENDING --> DEVELOPING : dispatch developer
DEVELOPING --> TESTING : DEV_DONE
TESTING --> DEVELOPING : FAIL
TESTING --> REVIEWING : PASS
REVIEWING --> DEVELOPING : REJECTED
REVIEWING --> GROUP_DONE : APPROVED
GROUP_DONE --> [*]
DEVELOPING --> ESCALATION : round >= 3
TESTING --> ESCALATION : fail >= 5
REVIEWING --> ESCALATION : reject >= 3
ESCALATION --> STUCK : architect dispatched
ESCALATION --> DEVELOPING : developer-deep PATH B
STUCK --> [*] : 人决策状态读取表(CLAUDE.md 的核心)
每个状态对应读哪个文件、看哪个字段:
| 状态 | 判定方式 |
|---|---|
PENDING |
这个组的 tasks 全是 - [ ] 且无 review/N.md |
DEVELOPING |
部分 - [x] 但未全部 |
DEV_DONE |
全部 - [x] |
TEST_PASSED |
test-reports/N.md 存在 + **Status:** PASS |
APPROVED |
review/N.md 存在 + **Status:** APPROVED |
STUCK |
STUCK.md 提到 group N |
GROUP_DONE |
DEV_DONE && TEST_PASSED && APPROVED |
→ main Claude 每轮重读文件判定状态,不缓存。
状态文件的 schema
每个状态文件必须有固定可解析格式:
# Review for Group 3
**Status:** APPROVED ← 这一行是状态机读的
**Reviewer Round:** 1 ← 这是计 round 数的
**Reviewed Commit:** abc123
## Findings
(具体内容)
## Required Changes
(仅 REJECTED 时)
格式必须严格——**Status:** APPROVED 中间有空格、有冒号、有星号,少一个 main Claude 都解析不到。
三个核心状态文件的关系
flowchart TB
Tasks["tasks.md
(checkbox 勾选状态)"] -.-> ST1["DEV 状态"]
TR["test-reports/N.md
**Status:** PASS|FAIL"] -.-> ST2["TEST 状态"]
Rev["review/N.md
**Status:** APPROVED|REJECTED"] -.-> ST3["REVIEW 状态"]
Stuck["STUCK.md
(架构师诊断)"] -.-> ST4["STUCK 状态"]
ST1 --> Combined["组 N 综合状态"]
ST2 --> Combined
ST3 --> Combined
ST4 --> Combined
style Combined fill:#c8e6c9一次状态判定的代码(伪)
main Claude 心里跑的逻辑:
def get_group_state(N):
if exists(f"STUCK.md") and N in STUCK.md:
return "STUCK"
tasks = parse_tasks_md()
group_tasks = tasks[N]
if all(t.checked for t in group_tasks):
dev_done = True
elif any(t.checked for t in group_tasks):
return "DEVELOPING"
else:
return "PENDING"
if exists(f"test-reports/{N}.md"):
report = read(f"test-reports/{N}.md")
if "**Status:** PASS" in report:
test_passed = True
else:
return "DEVELOPING (回炉)"
else:
return "TESTING (待跑)"
if exists(f"review/{N}.md"):
review = read(f"review/{N}.md")
if "**Status:** APPROVED" in review:
return "GROUP_DONE"
else:
return "DEVELOPING (回炉)"
else:
return "REVIEWING (待评)"
→ 这就是 dev.md Step 3 在做的事。
Crash recovery 演示
你跑 /dev 1
↓
组 1 跑到 REVIEWING 阶段
↓
你机器突然死机 / Claude Code 崩溃
↓
重启 Claude Code
你: /dev 1
↓
main Claude 读文件:
- tasks.md 显示 1.1~1.4 全勾
- test-reports/1.md 有 **Status:** PASS
- review/1.md 不存在
↓
判定: 状态 = REVIEWING (待评)
↓
直接 dispatch reviewer,不重做前面的工作
→ 完美恢复,没丢一步。
这套设计的隐藏好处
✅ git diff 即可看出 "这次跑改了什么状态"
✅ git log 即历史记录,知道每个 round 何时发生
✅ 状态可被外部脚本读取(监控、报表)
✅ 切换设备工作(笔记本 → 台式机)零成本
✅ 多人协作(A 跑了一半,B 接上)只要看文件
反模式
❌ main Claude 用对话历史记状态
→ /clear 后丢失
❌ 状态文件没固定 schema
→ "**Status: APPROVED**" 和 "**Status:** APPROVED" 都有
→ 解析失败
❌ 用 emoji 当状态标记 ("✅ 完成")
→ 看着好看,但解析脆弱
❌ 状态分散在多个无关文件
→ 还原状态要读 10 个文件 = 心累
❌ 写状态又改变状态
→ e.g. reviewer.md 写完 review 后又改 tasks.md → 角色越界
你现在能做什么
- 画出自己项目的状态机
- 设计每个状态文件的 schema
- 解释 crash-safe 是怎么实现的
- 知道为什么要"每轮重读文件而不是缓存"
flowchart LR
Ch1to10["Ch 1~10
第一个 change
(知识层基础)"] --> Ch11to13["Ch 11~13
spec/design/tasks
写到能跑"]
Ch11to13 --> Ch14to18["Ch 14~18
多 agent 设计
含升级链"]
Ch14to18 --> Ch19to20["Ch 19~20
CLAUDE.md
+ 状态机"]
Ch19to20 --> You["✓ 拥有完整
'治理层 + 知识层'
等待和工具层结合"]
style You fill:#c8e6c9核心检查点:现在你应该已经能:
- 为自己的项目写一份 4 件套(proposal/design/spec/tasks)
- 设计 4~6 个角色的 agent 文件
- 写出 CLAUDE.md 让 main Claude 按规则办事
- 解释整个状态机和升级链如何工作
如果还没——回到对应章节重做。