两小时工作效率分析与改进建议
好问题。让我先看看 OpenSpec 实际能做什么,再给你判断。 好,现在我清楚了。先给你一个直接的判断,然后解决你的4个问题。
好问题。让我先看看 OpenSpec 实际能做什么,再给你判断。
Searched the web
Searched the web
好,现在我清楚了。先给你一个直接的判断,然后解决你的4个问题。
先说结论:OpenSpec 该不该用
OpenSpec 解决的核心问题是:每个功能变更有结构化的提案→设计→任务→归档记录,这恰好覆盖你的问题2、4。
它不解决的:项目背景知识库(问题1)、验收标准执行(问题3)。
所以答案是:用 OpenSpec 管 Spec 层,用上一版方案管 Agent 执行层,两者组合。不是二选一。
修订后的完整目录结构
your-project/
├── CLAUDE.md ← Agent调度规范(不变)
├── LESSONS.md ← 经验沉淀(不变)
│
├── openspec/ ← OpenSpec接管Spec层
│ ├── project.md ← ★ 问题1:项目背景知识库
│ ├── AGENTS.md ← AI读取规范的入口
│ ├── specs/ ← ★ 问题4:当前系统完整状态
│ │ ├── features/ ← 已上线功能的Spec
│ │ └── architecture.md ← 架构决策记录
│ └── changes/ ← ★ 问题2:需求记录
│ ├── active/
│ │ └── feat-xxx/
│ │ ├── proposal.md ← 为什么做,背景,范围
│ │ ├── specs/ ← GIVEN/WHEN/THEN验收场景
│ │ ├── design.md ← 技术方案(讨论并锁定)
│ │ └── tasks.md ← 实现拆解清单
│ └── archive/ ← 历史变更存档
│ └── 2025-01-23-feat-xxx/ ← 完成后合并进specs/
│
└── .claude/
├── agents/ ← Agent定义(同上一版)
└── commands/
├── new-feature.md ← 新需求入口命令
└── run-issue.md ← 执行命令(调整)
问题1:项目背景知识在哪里积累
文件:openspec/project.md
初始化时让 AI 内省项目,填写 openspec/project.md,产出约250行的结构化项目描述。 Hashrocket第一次运行:
bash
npm install -g @fission-ai/openspec
cd your-project
openspec init # 选择 Claude Code
# 然后在 Claude Code 里:
# "请内省我的项目,填写 openspec/project.md"
project.md 的内容结构:
markdown
# 项目背景
## 业务背景
- 这个项目是什么,解决什么问题
- 目标用户是谁
- 核心业务约束(如:合规要求、性能红线)
## 技术栈
- 前端:...
- 后端:...
- 数据库:...
- 主要依赖库及版本
## 架构原则
- 我们遵守的设计决策(如:所有API必须版本化)
- 我们主动避免的模式(如:不用全局状态)
## 代码规范
- 命名约定
- 错误处理方式
- 测试覆盖要求
## 关键模块说明
- 模块A:负责什么,核心逻辑在哪里
- 模块B:...
关键:这个文件是活文档,每次架构决策变化时更新它。 所有 Agent 启动时都会读 openspec/AGENTS.md,里面指向 project.md,确保每个 Agent 都有项目背景。
问题2:需求信息从哪些维度记录,方案是否讨论
每个需求对应 openspec/changes/active/{feature-name}/ 下四个文件。
流程是:先跑 /opsx:propose,人工审查确认,再跑 Agent 执行。
proposal.md — 为什么做
markdown
## 背景
用户反馈登录超时没有提示,直接跳转导致数据丢失。
## 目标
用户在 token 过期前5分钟收到警告,可以选择续期或保存。
## 范围
- 涉及:auth模块、前端弹窗组件
- 不涉及:后端session存储(不改)
## 风险
- 与现有的"记住我"功能有交互,需要验证
specs/ — ★ 问题3的核心,GIVEN/WHEN/THEN 格式的验收场景
markdown
## 场景1:正常续期
GIVEN 用户已登录且token剩余5分钟
WHEN 系统检测到剩余时间
THEN 弹出续期提示框
AND 用户点击续期后token刷新成功
AND 提示框消失
## 场景2:用户忽略提示
GIVEN 弹出续期提示
WHEN 用户不操作,token过期
THEN 自动登出
AND 跳转到登录页
AND 显示"会话已过期"提示,不丢失当前表单数据
## 场景3:边界——已是最长续期
GIVEN 用户已达到最大续期次数(3次)
WHEN token即将过期
THEN 不显示续期选项
AND 只显示"请保存工作后重新登录"
design.md — 技术方案讨论并锁定
markdown
## 方案选择
### 方案A:前端轮询(推荐)
- 每60秒检查一次token剩余时间
- 优点:简单,不需要WebSocket
- 缺点:有最多60秒延迟
### 方案B:后端推送
- 需要WebSocket支持
- 优点:精确
- 缺点:增加架构复杂度,当前项目不值得
## 决定:选方案A
## 实现细节
- 检查逻辑放在 AuthService.checkTokenExpiry()
- 弹窗组件:SessionWarningModal
- 存储续期次数:localStorage key='renewal_count'
tasks.md — 实现拆解,Agent 直接按这个执行
markdown
- [ ] 1.1 AuthService 增加 checkTokenExpiry() 方法
- [ ] 1.2 在 App.tsx 挂载时启动定时器
- [ ] 2.1 创建 SessionWarningModal 组件
- [ ] 2.2 实现续期按钮调用 refreshToken API
- [ ] 3.1 编写场景1的集成测试
- [ ] 3.2 编写场景2的集成测试
- [ ] 3.3 编写场景3的边界测试
问题3:验收标准怎么执行,怎么判断通过
specs/ 里的 GIVEN/WHEN/THEN 就是验收标准,tester Agent 直接对照执行。
修改 tester.md Agent:
yaml
---
name: tester
description: 测试验证Agent
model: sonnet
tools: Read, Write, Edit, Bash, Grep, Glob
---
执行前,先读取:
1. openspec/changes/active/{feature}/specs/ — 这是验收标准,必须全部通过
2. openspec/project.md — 了解测试框架
验收流程:
1. 对每个 GIVEN/WHEN/THEN 场景,编写对应的测试用例
2. 场景描述即测试用例名称,保持一致,便于追溯
3. 运行测试
4. 输出验收报告:
- 场景X:PASS / FAIL(附失败原因)
5. 所有场景PASS → 输出 ACCEPTANCE_PASSED
6. 任何场景FAIL → 最多重试3次修复 → 仍失败输出 ACCEPTANCE_FAILED
判断通过的标准:specs/里每一个场景对应的测试用例全部为绿。
这样验收就是客观的:不是"感觉差不多",而是场景1通过、场景2通过、场景3通过,全部绿灯才算验收通过。
问题4:长期迭代,之前的功能怎么追溯
OpenSpec 把当前系统状态(specs/)和历史变更(archive/)分开存放,archive 里保留每次变更的完整提案和设计,形成审计追踪,让人能理解每个决策是如何和为何做出的。 Intent-driven
每次 /opsx:archive 执行后:
- 这次变更的 proposal/specs/design/tasks 打包进
archive/2025-01-23-feat-xxx/ - delta spec 合并进
openspec/specs/features/,更新当前系统状态
六个月后想知道"登录续期是怎么做的":
openspec/
├── specs/features/auth.md ← 当前状态(含续期功能描述)
└── archive/2025-01-23-feat-login-renewal/
├── proposal.md ← 当时为什么做
├── design.md ← 当时选了什么方案,为什么
└── specs/ ← 当时的验收标准
openspec list 可以看所有历史变更列表,openspec show feat-login-renewal 可以看详情。
完整工作流(修订版)
需求来了
↓
/opsx:propose {feature-name} ← OpenSpec 生成提案结构
↓
人工填写/审查 proposal.md + specs/ + design.md
(这是唯一需要人工介入的环节)
↓
/run-issue {feature-name} ← Claude Code Agent 接管
├── analyst 读 design.md → 确认实现路径
├── coder 读 tasks.md → 编码
├── tester 读 specs/ → 对照验收场景测试
├── reviewer 审查
└── archiver 提交 + 触发 /opsx:archive
↓
openspec/specs/ 更新,archive/ 存档
↓
下一个需求
安装 OpenSpec 只需要两行命令:
bash
npm install -g @fission-ai/openspec
openspec init # 在项目根目录,选 Claude Code
然后 openspec/AGENTS.md 会自动生成,Claude Code 的所有 Agent 都能读到它,不需要额外配置。
##user
我想让coder和test使用不同模型,这样可以相互印证,还需要做哪些可以让他们
- 流程衔接顺畅
- 同用文件系统的记录,比如feature的描述、技术实现设定、测试范围等
- 出现问题可以相互告知。
Claude responded: 好问题,这是多Agent协作最精细的地方。先搞清楚一个关键限制。 有一个重要的现实情况需要先告诉你,再给方案。
好问题,这是多Agent协作最精细的地方。先搞清楚一个关键限制。
Searched the web
Searched the web
有一个重要的现实情况需要先告诉你,再给方案。
先说清楚一个坑
Claude Code 目前对 model: frontmatter 字段的支持存在争议——有 issue 记录显示该字段在运行时并不被实际尊重,CLAUDE_CODE_SUBAGENT_MODEL 环境变量会覆盖所有 agent 的模型设置。 GitHub
还有实测记录显示:即使 Opus 主会话注册了 Sonnet subagent,在7次运行中 delegation 工具调用次数为零——subagent 被读取、解析、列出,但从未被真正调用。 AkitaOnRails
所以"coder 用 Sonnet、tester 用 Opus"这个目标,现在有两条路:
- 路线A:继续用 Claude Code 原生 subagent,接受模型路由不稳定的现实,靠文件系统协议解决你的3个问题(衔接、共享记录、互告问题)
- 路线B:用两个独立的
claude -p进程,各自传--model参数,用文件系统作为进程间通信,彻底绕开模型路由问题
推荐路线B,原因:模型指定100%可控,文件协议反而更清晰。下面给你完整方案。
核心设计:共享工作目录 + 状态文件协议
两个 Agent 用同一套目录结构通信,不靠函数调用,只靠读写文件。
.claude/work/{feature-id}/
├── context.md ← 共享上下文(两个Agent都读)
├── coder-output.md ← Coder写,Tester读
├── test-report.md ← Tester写,Coder读(失败时)
└── handshake.md ← 状态机:记录当前在哪步、出了什么问题
第一步:context.md — 两个Agent的共同语言
这个文件在 coder 启动前由 orchestrator 生成,两个 Agent 启动时必须先读这一个文件。
markdown
# Feature Context: {feature-id}
## 来源
- OpenSpec: openspec/changes/active/{feature-id}/
- 任务清单: openspec/changes/active/{feature-id}/tasks.md
## 功能描述
{从 proposal.md 摘取的一句话描述}
## 技术实现约定
{从 design.md 摘取的关键决策,例如:}
- 用方案A(前端轮询),不用WebSocket
- 新增 AuthService.checkTokenExpiry() 方法
- 弹窗组件命名:SessionWarningModal
- localStorage key: 'renewal_count'
## 验收场景(Tester必须全部覆盖)
{从 openspec/changes/active/{feature-id}/specs/ 完整复制}
- 场景1: GIVEN...WHEN...THEN
- 场景2: ...
- 场景3: ...
## 文件边界
Coder 负责写:
- src/services/AuthService.ts
- src/components/SessionWarningModal.tsx
Tester 负责写:
- tests/auth/session-renewal.test.ts
## 禁区(两个Agent都不得碰)
- src/legacy/ (旧模块,本次不动)
- database/migrations/ (本次不涉及)
为什么这么做:coder 和 tester 的上下文窗口是独立的,它们之间唯一可靠的信息通道就是文件。把所有约定写进 context.md,等于给两个 Agent 一份相同的"工作说明书",避免各自理解偏差。
第二步:handshake.md — 状态机和问题通知
这是两个 Agent 互告问题的机制,格式要固定,方便 orchestrator 脚本解析。
markdown
# Handshake: {feature-id}
## 当前状态
STATUS: CODER_DONE
# 状态枚举:CODER_RUNNING | CODER_DONE | CODER_BLOCKED
# TESTER_RUNNING | TESTER_DONE | TESTER_BLOCKED | ACCEPTED
## Coder → Tester 交接
CODER_COMPLETE_TIME: 2025-01-23 14:32
CHANGED_FILES:
- src/services/AuthService.ts (新增 checkTokenExpiry 方法,L45-L78)
- src/components/SessionWarningModal.tsx (新增组件)
CODER_NOTES:
- checkTokenExpiry 返回秒数,-1 表示已过期,调用方自行判断阈值
- SessionWarningModal 接受 onRenew / onDismiss 两个 callback prop
- 注意:renewal_count 存在 localStorage,Tester 测试前需要 clear
## Tester → Coder 反馈(测试失败时填写)
TEST_FAIL_TIME:
FAILED_SCENARIOS:
FAIL_DETAILS:
REPRODUCTION_STEPS:
SUSPECTED_CAUSE:
Coder 完成后写:填 STATUS: CODER_DONE + CODER_NOTES,然后停止,不再动文件。
Tester 读到 CODER_DONE 后启动,测试失败时回填 Tester → Coder 区块,STATUS 改为 TESTER_BLOCKED。
Orchestrator 轮询这个文件决定下一步派谁上。
第三步:两个独立进程的启动脚本
scripts/run-feature.sh
bash
#!/bin/bash
FEATURE=$1 # 例如:feat-session-renewal
WORK_DIR=".claude/work/$FEATURE"
SPEC_DIR="openspec/changes/active/$FEATURE"
mkdir -p "$WORK_DIR"
# 1. 生成 context.md(orchestrator 负责,用 Sonnet 即可)
claude -p --model claude-sonnet-4-6 "
读取以下文件,生成 $WORK_DIR/context.md:
- $SPEC_DIR/proposal.md
- $SPEC_DIR/design.md
- $SPEC_DIR/specs/
- $SPEC_DIR/tasks.md
格式严格按照 .claude/templates/context-template.md
"
# 2. 启动 Coder(Sonnet,擅长执行)
echo "STATUS: CODER_RUNNING" > "$WORK_DIR/handshake.md"
claude -p --model claude-sonnet-4-6 \
--dangerously-skip-permissions \
"你是 Coder Agent。
第一步必须读取:$WORK_DIR/context.md
按 tasks.md 清单逐项实现,完成后:
1. 在 $WORK_DIR/coder-output.md 记录实现摘要
2. 在 $WORK_DIR/handshake.md 填写 CODER_DONE 区块
3. 将 STATUS 改为 CODER_DONE
4. 停止,不要继续" 2>&1 | tee "$WORK_DIR/coder.log"
# 3. 等待 Coder 完成(简单轮询)
while ! grep -q "STATUS: CODER_DONE" "$WORK_DIR/handshake.md" 2>/dev/null; do
sleep 10
done
echo "Coder 完成,启动 Tester..."
# 4. 启动 Tester(Opus,擅长质疑和边界发现)
echo "STATUS: TESTER_RUNNING" >> "$WORK_DIR/handshake.md"
claude -p --model claude-opus-4-7 \
--dangerously-skip-permissions \
"你是 Tester Agent,使用不同于 Coder 的模型,目的是独立验证。
第一步必须读取:
1. $WORK_DIR/context.md (理解验收场景和技术约定)
2. $WORK_DIR/handshake.md (读取 Coder 的交接说明)
任务:
- 对 context.md 中每个验收场景,编写并运行对应测试
- 特别关注 handshake.md 里 CODER_NOTES 提到的注意事项
- 不要修改 Coder 写的业务代码,只写测试文件
测试全部通过:
- 写 $WORK_DIR/test-report.md,STATUS 改为 TESTER_DONE
有场景失败(重试3次后):
- 在 handshake.md 填写 Tester→Coder 反馈区块
- STATUS 改为 TESTER_BLOCKED
- 停止" 2>&1 | tee "$WORK_DIR/tester.log"
# 5. 处理结果
STATUS=$(grep "^STATUS:" "$WORK_DIR/handshake.md" | tail -1 | cut -d' ' -f2)
if [ "$STATUS" = "TESTER_DONE" ]; then
echo "✅ 验收通过,触发 archiver..."
claude -p --model claude-haiku-4-5 \
"读取 $WORK_DIR/handshake.md,执行 git add/commit/push,
然后运行 /opsx:archive $FEATURE,
追加经验到 LESSONS.md"
elif [ "$STATUS" = "TESTER_BLOCKED" ]; then
echo "❌ 测试失败,重启 Coder 修复..."
# 重新触发 Coder,传入 test-report
# (最多重试2次,超过则人工介入)
fi
第四步:Coder 和 Tester 的 Agent 定义
.claude/agents/coder.md(执行角色,用 Sonnet)
yaml
---
name: coder
description: 编码实现Agent。接到任务后严格按 context.md 的技术约定实现,完成后写 handshake.md。
model: sonnet
tools: Read, Write, Edit, Bash, Grep, Glob
---
你的工作方式:
启动时,必须先读:
1. .claude/work/{feature}/context.md — 理解约定和边界
2. openspec/changes/active/{feature}/tasks.md — 你的任务清单
实现原则:
- context.md 里写了用哪个方案就用哪个方案,不要自己发明
- 只改"文件边界"里属于你的文件,不碰禁区
- 每完成一个 task,在 tasks.md 里打勾 [x]
完成后写 handshake.md 的 CODER_DONE 区块:
- 列出改了哪些文件的哪些行
- 写清楚 Tester 需要知道的注意事项(如:测试前需要 mock 什么)
- 把 STATUS 改为 CODER_DONE
如果收到 TESTER_BLOCKED 通知(handshake.md 里有 Tester 反馈):
- 先读 FAIL_DETAILS 和 REPRODUCTION_STEPS
- 修复后更新 handshake.md,STATUS 改回 CODER_DONE
.claude/agents/tester.md(质疑角色,用 Opus)
yaml
---
name: tester
description: 测试验证Agent。独立于Coder进行验证,以质疑者视角检验实现是否符合验收场景。
model: opus
tools: Read, Write, Edit, Bash, Grep, Glob
---
你的角色定位:你是独立质检员,不是 Coder 的助手。
启动时,必须先读:
1. .claude/work/{feature}/context.md — 验收场景是你的唯一标准
2. .claude/work/{feature}/handshake.md — 读 Coder 的交接说明
测试策略(比 Coder 更严格):
- 每个 GIVEN/WHEN/THEN 场景必须有对应的测试用例,名称要一致
- 重点测试 CODER_NOTES 里提到的边界情况
- 主动寻找场景之间的交互问题(场景A + 场景B 同时发生会怎样)
- 测试数据不要用魔法数字,要能解释为什么选这个值
失败时的反馈格式(写进 handshake.md):
FAILED_SCENARIOS: 场景2
FAIL_DETAILS: 当 renewal_count=3 时,弹窗仍然显示续期按钮
REPRODUCTION_STEPS:
1. localStorage.setItem('renewal_count', '3')
2. 触发 token 即将过期
3. 观察弹窗内容
SUSPECTED_CAUSE: SessionWarningModal 没有读取 renewal_count,
只收了 onRenew prop,判断逻辑应该在组件内部
这个反馈要让 Coder 不问你任何问题就能直接复现和修复。
整体流程图
/run-feature feat-xxx
↓
生成 context.md
(两个Agent的共同语言)
↓
Coder (Sonnet) 启动
读 context.md + tasks.md
↓ 写 handshake.md: CODER_DONE
↓ 写 coder-output.md
↓
Tester (Opus) 启动
读 context.md + handshake.md
独立对照验收场景测试
↓
┌── PASS ──→ handshake: TESTER_DONE → Archiver
│
└── FAIL ──→ handshake: TESTER_BLOCKED
Coder 读反馈,修复
最多循环2次
超过 → 人工介入
为什么这样设计能解决你的3个问题
衔接顺畅:Tester 不靠 Coder 口头汇报,靠读文件。文件在,衔接必然发生;文件不在,脚本不启动 Tester。状态机清晰,没有歧义。
共用文件系统记录:context.md 是单一事实来源。feature 描述、技术约定、测试范围,全部在这一个文件里,两个 Agent 读的是同一份,不会各自理解一套。
出现问题相互告知:handshake.md 是专用的问题通道。Tester 失败时必须写够信息(复现步骤 + 怀疑原因),格式固定,Coder 重启时强制先读这个文件,不会出现"不知道哪里失败了"的情况。
