sillyspec 3.20.0 → 3.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.npmrc-tmp CHANGED
@@ -1 +1 @@
1
- //registry.npmjs.org/:_authToken=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
1
+ //registry.npmjs.org/:_authToken=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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.20.0",
3
+ "version": "3.20.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -37,4 +37,4 @@
37
37
  "sql.js": "^1.14.1",
38
38
  "ws": "^8.18"
39
39
  }
40
- }
40
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * brainstorm-postcheck.js — plan 阶段对 brainstorm 产物的校验
3
+ *
4
+ * 在 brainstorm → plan 之间执行,校验 brainstorm 产物完整性。
5
+ * PASS → 继续 plan
6
+ * WARN → 继续 plan,记录警告并分配补齐任务
7
+ * FAIL → 回退到 brainstorm 补齐(最多 2 次重试)
8
+ */
9
+
10
+ import { existsSync, readFileSync } from 'fs'
11
+ import { join } from 'path'
12
+
13
+ /**
14
+ * 校验 brainstorm 产物完整性
15
+ * @param {string} changeDir - 变更目录路径(.sillyspec/changes/<change>)
16
+ * @returns {{ ok: boolean, level: 'PASS'|'WARN'|'FAIL', errors: string[], warnings: string[] }}
17
+ */
18
+ export function checkBrainstormArtifacts(changeDir) {
19
+ const errors = []
20
+ const warnings = []
21
+ const brainstormDir = join(changeDir, 'brainstorm')
22
+
23
+ // ── 1. 产物完整性检查 ──
24
+ const requiredFiles = ['design.md', 'decisions.md', 'gaps.md', 'assumptions.md', 'next-action.json']
25
+ for (const file of requiredFiles) {
26
+ if (!existsSync(join(brainstormDir, file))) {
27
+ errors.push(`brainstorm/${file} 不存在`)
28
+ }
29
+ }
30
+
31
+ // ── 2. next-action.json 有效性检查 ──
32
+ const nextActionFile = join(brainstormDir, 'next-action.json')
33
+ if (existsSync(nextActionFile)) {
34
+ try {
35
+ const nextAction = JSON.parse(readFileSync(nextActionFile, 'utf8'))
36
+
37
+ // 必填字段
38
+ if (!['ready_for_plan', 'waiting_for_user'].includes(nextAction.status)) {
39
+ errors.push(`next-action.json.status 无效值: ${nextAction.status}`)
40
+ }
41
+ if (typeof nextAction.has_blocking_questions !== 'boolean') {
42
+ errors.push(`next-action.json.has_blocking_questions 不是布尔值`)
43
+ }
44
+ if (!['high', 'medium', 'low'].includes(nextAction.decision_level)) {
45
+ warnings.push(`next-action.json.decision_level 无效值: ${nextAction.decision_level}`)
46
+ }
47
+
48
+ // 状态一致性
49
+ if (nextAction.status === 'waiting_for_user' && nextAction.has_blocking_questions !== true) {
50
+ errors.push(`next-action.json status=waiting_for_user 但 has_blocking_questions !== true`)
51
+ }
52
+ if (nextAction.status === 'ready_for_plan' && nextAction.has_blocking_questions === true) {
53
+ errors.push(`next-action.json status=ready_for_plan 但 has_blocking_questions === true`)
54
+ }
55
+ } catch (e) {
56
+ errors.push(`next-action.json 解析失败: ${e.message}`)
57
+ }
58
+ }
59
+
60
+ // ── 3. design.md 覆盖度检查 ──
61
+ const designFile = join(brainstormDir, 'design.md')
62
+ if (existsSync(designFile)) {
63
+ const designContent = readFileSync(designFile, 'utf8')
64
+
65
+ // 必须覆盖的维度
66
+ if (!/(?:目标|goal|objective|背景|background|问题|problem)/i.test(designContent)) {
67
+ warnings.push('design.md 缺少设计目标/背景描述')
68
+ }
69
+ if (!/(?:范围|scope|总体方案|方案|approach)/i.test(designContent)) {
70
+ warnings.push('design.md 缺少总体方案描述')
71
+ }
72
+
73
+ // 弱覆盖检测
74
+ if (/影响模块.*TBD|文件变更.*TBD|待定/i.test(designContent)) {
75
+ warnings.push('design.md 包含 TBD/待定内容')
76
+ }
77
+ if (!/(?:验收|acceptance|完成标准)/i.test(designContent)) {
78
+ warnings.push('design.md 缺少验收标准')
79
+ }
80
+ }
81
+
82
+ // ── 4. gaps.md 覆盖度检查 ──
83
+ const gapsFile = join(brainstormDir, 'gaps.md')
84
+ if (existsSync(gapsFile)) {
85
+ const gapsContent = readFileSync(gapsFile, 'utf8')
86
+ const hasBlocker = /BLOCKER/i.test(gapsContent)
87
+ if (hasBlocker) {
88
+ errors.push('gaps.md 包含 BLOCKER 级缺口,需要先解决')
89
+ }
90
+ // 检查是否为空
91
+ const nonEmpty = gapsContent.split('\n').filter(l => l.trim() && !l.startsWith('#')).length
92
+ if (nonEmpty === 0) {
93
+ warnings.push('gaps.md 为空(brainstorm 可能遗漏了缺口分析)')
94
+ }
95
+ }
96
+
97
+ // ── 5. assumptions.md 风险检查 ──
98
+ const assumptionsFile = join(brainstormDir, 'assumptions.md')
99
+ if (existsSync(assumptionsFile)) {
100
+ const assumptionsContent = readFileSync(assumptionsFile, 'utf8')
101
+ if (/假设现有数据格式不变|假设.*API.*不变|假设.*性能.*可接受/i.test(assumptionsContent)) {
102
+ warnings.push('assumptions.md 包含高风险假设,需在 verify 阶段验证')
103
+ }
104
+ }
105
+
106
+ // ── 6. decisions.md 一致性检查 ──
107
+ const decisionsFile = join(brainstormDir, 'decisions.md')
108
+ if (existsSync(decisionsFile)) {
109
+ const decisionsContent = readFileSync(decisionsFile, 'utf8')
110
+ const autoDecided = (decisionsContent.match(/AUTO_DECIDED/g) || []).length
111
+ const autoDecidedWithReason = (decisionsContent.match(/AUTO_DECIDED[\s\S]*?checklist/i) || []).length
112
+ if (autoDecided > 0 && autoDecidedWithReason < autoDecided) {
113
+ errors.push(`${autoDecided - autoDecidedWithReason} 个 AUTO_DECIDED 决策缺少 checklist 依据`)
114
+ }
115
+ }
116
+
117
+ // ── 判定结果 ──
118
+ let level
119
+ if (errors.length > 0) {
120
+ level = 'FAIL'
121
+ } else if (warnings.length > 0) {
122
+ level = 'WARN'
123
+ } else {
124
+ level = 'PASS'
125
+ }
126
+
127
+ return { ok: level !== 'FAIL', level, errors, warnings }
128
+ }
129
+
130
+ /**
131
+ * 执行 brainstorm postcheck(供 run.js 调用)
132
+ */
133
+ export async function executeBrainstormPostcheck(cwd, platformOpts, changeName) {
134
+ const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
135
+ const changeDir = join(specBase, 'changes', changeName)
136
+ if (!existsSync(changeDir)) {
137
+ console.log('⚠️ brainstorm postcheck: 变更目录不存在,跳过')
138
+ return
139
+ }
140
+
141
+ const result = checkBrainstormArtifacts(changeDir)
142
+
143
+ if (result.level === 'PASS') {
144
+ console.log('✅ brainstorm postcheck: PASS')
145
+ } else if (result.level === 'WARN') {
146
+ console.log('⚠️ brainstorm postcheck: WARN')
147
+ for (const w of result.warnings) {
148
+ console.log(` - ${w}`)
149
+ }
150
+ } else {
151
+ console.error('❌ brainstorm postcheck: FAIL')
152
+ for (const e of result.errors) {
153
+ console.error(` - ${e}`)
154
+ }
155
+ console.error('\\n 请修复以上问题后重试,或使用 --skip-approval 跳过。')
156
+ process.exit(1)
157
+ }
158
+ }
@@ -1,21 +1,63 @@
1
1
  /**
2
2
  * change-risk-profile.js — 变更风险分级检测
3
3
  *
4
- * 根据变更涉及的文件类型、关键词,自动判定 verify 所需的验收强度。
4
+ * 根据变更涉及的文件类型、关键词、git diff、brainstorm 产物,
5
+ * 自动判定 P0/P1/P2 风险等级,产出结构化 risk-profile.json。
6
+ *
7
+ * P0 = 阻塞确认(必须用户确认)
8
+ * P1 = 自动推进但记录
9
+ * P2 = 自动通过
5
10
  */
6
11
 
7
- /**
8
- * 触发 integration-critical 风险等级的关键词
9
- */
10
- const INTEGRATION_CRITICAL_PATTERNS = [
11
- // 跨进程通信
12
+ // ============ P0 触发规则 ============
13
+
14
+ const P0_FILE_PATTERNS = [
15
+ { id: 'R-002', pattern: /(?:^|\/)(?:migrations?|migration)\/(?:.+)/i, desc: '数据库 migration' },
16
+ { id: 'R-002', pattern: /(?:^|\/)schema\.(?:sql|prisma|ts|js)$/i, desc: '数据库 schema' },
17
+ { id: 'R-003', pattern: /(?:^|\/)(?:auth|permission|role|login|token)\b/i, desc: '鉴权/权限' },
18
+ { id: 'R-004', pattern: /(?:^|\/)(?:payment|billing|stripe|paypal)\b/i, desc: '支付/资金' },
19
+ { id: 'R-005', pattern: /\.(?:env|env\.\w+)$/, desc: '环境配置' },
20
+ { id: 'R-005', pattern: /(?:^|\/)(?:Dockerfile|docker-compose\.\w+)$/i, desc: '部署配置' },
21
+ { id: 'R-005', pattern: /(?:^|\/)config\.(?:yaml|yml|json|toml)$/i, desc: '生产配置' },
22
+ ]
23
+
24
+ const P0_CONTENT_PATTERNS = [
25
+ { id: 'R-001', pattern: /\b(?:DROP\s+TABLE|TRUNCATE\s+TABLE|DELETE\s+FROM\s+\w+\s+(?!WHERE\s+.+\s+LIMIT))/i, desc: '删除数据' },
26
+ { id: 'R-002', pattern: /\b(?:CREATE\s+TABLE|ALTER\s+TABLE|ADD\s+COLUMN|DROP\s+COLUMN|CREATE\s+INDEX)/i, desc: '数据库 migration' },
27
+ ]
28
+
29
+ // ============ P1 触发规则 ============
30
+
31
+ const P1_FILE_PATTERNS = [
32
+ { id: 'R-101', pattern: /(?:^|\/)(?:routes|controllers?|handlers?)\//i, desc: 'API 层' },
33
+ { id: 'R-102', pattern: /(?:^|\/)(?:models|entities)\//i, desc: '数据模型层' },
34
+ { id: 'R-104', pattern: /(?:^|\/)(?:services)\//i, desc: '业务逻辑层(跨模块)' },
35
+ { id: 'R-106', pattern: /(?:^|\/)index\.(?:ts|js)$/i, desc: 're-export 入口' },
36
+ ]
37
+
38
+ const P1_CONTENT_PATTERNS = [
39
+ { id: 'R-101', pattern: /\b(?:daemon|backend|grpc|websocket|cross.?process|ipc|message.?queue)\b/i, desc: '跨进程通信' },
40
+ { id: 'R-101', pattern: /\b(?:session|lease|agent.?run|lifecycle|state.?transition|claim|heartbeat)\b/i, desc: '状态机/生命周期' },
41
+ { id: 'R-102', pattern: /\b(?:api|client|contract|dto)\b/i, desc: 'API contract' },
42
+ { id: 'R-105', pattern: /(?:^|\/)(?:workflow|daemon|session|lifecycle|state-machine)\b/i, desc: '核心模块' },
43
+ ]
44
+
45
+ // ============ P0/P1 命中文件名关键词(用于 git diff 检测) ============
46
+
47
+ const PROTECTED_FILE_NAMES = [
48
+ '.env', 'package.json', 'tsconfig.json', 'vite.config.ts', 'vite.config.js',
49
+ 'prisma/schema.prisma', 'docker-compose.yml', 'docker-compose.yaml',
50
+ ]
51
+
52
+ // ============ 向后兼容:旧的 INTEGRATION_CRITICAL_PATTERNS ============
53
+
54
+ export const INTEGRATION_CRITICAL_PATTERNS = [
12
55
  /\bdaemon\b/i,
13
56
  /\bbackend\b/i,
14
57
  /\bclient.*api\b/i,
15
58
  /\bgrpc\b/i,
16
59
  /\bwebsocket\b/i,
17
60
  /\bhttp.*client\b/i,
18
- // 状态机 / 生命周期
19
61
  /\bsession\b/i,
20
62
  /\blease\b/i,
21
63
  /\bagent.?run\b/i,
@@ -23,12 +65,10 @@ const INTEGRATION_CRITICAL_PATTERNS = [
23
65
  /\bstate.?transition\b/i,
24
66
  /\bclaim\b/i,
25
67
  /\bheartbeat\b/i,
26
- // 跨进程协议
27
68
  /\bcross.?process\b/i,
28
69
  /\bipc\b/i,
29
70
  /\bmessage.?queue\b/i,
30
71
  /\bpub.?sub\b/i,
31
- // 部署/启动路径
32
72
  /\bcli\.ts\b/i,
33
73
  /\bmain\.ts\b/i,
34
74
  /\bentrypoint\b/i,
@@ -38,10 +78,7 @@ const INTEGRATION_CRITICAL_PATTERNS = [
38
78
  /\bdocker.?compose\b/i,
39
79
  ]
40
80
 
41
- /**
42
- * 需要集成验证的文件路径关键词
43
- */
44
- const INTEGRATION_FILE_PATTERNS = [
81
+ export const INTEGRATION_FILE_PATTERNS = [
45
82
  /daemon/i,
46
83
  /session.?manager/i,
47
84
  /agent.?run/i,
@@ -55,12 +92,163 @@ const INTEGRATION_FILE_PATTERNS = [
55
92
  /startup/i,
56
93
  ]
57
94
 
95
+ // ============ 核心检测函数 ============
96
+
58
97
  /**
59
- * 检测变更风险等级
98
+ * 三级风险检测:P0 / P1 / P2
60
99
  * @param {object} opts
61
100
  * @param {string} [opts.designContent] - design.md 内容
62
101
  * @param {string} [opts.planContent] - plan.md 内容
63
102
  * @param {string[]} [opts.changedFiles] - 变更文件列表
103
+ * @param {string} [opts.diffContent] - git diff 内容(可选,用于更精确检测)
104
+ * @param {object} [opts.nextAction] - next-action.json 解析后的对象(可选)
105
+ * @param {string[]} [opts.protectedFiles] - 项目自定义 protected files 列表
106
+ * @returns {RiskProfile}
107
+ *
108
+ * @typedef {object} RiskProfile
109
+ * @property {string} level - 'P0' | 'P1' | 'P2'
110
+ * @property {string[]} triggers - 触发的规则 ID 列表
111
+ * @property {object[]} assessedFrom - 评估依据
112
+ * @property {string[]} applyBlockers - apply 阻塞原因
113
+ * @property {boolean} canAutoApply - 是否可自动 apply
114
+ * @property {string} applyReason - apply 决策原因
115
+ */
116
+ export function detectRiskProfile({ designContent = '', planContent = '', changedFiles = [], diffContent = '', nextAction = null, protectedFiles = [] } = {}) {
117
+ const triggers = []
118
+ const assessedFrom = []
119
+
120
+ const combined = [designContent, planContent].join('\n')
121
+
122
+ // ── P0 检测(最高优先级)──
123
+
124
+ // 基于文件路径
125
+ for (const rule of P0_FILE_PATTERNS) {
126
+ for (const file of changedFiles) {
127
+ if (rule.pattern.test(file)) {
128
+ rule.pattern.lastIndex = 0
129
+ if (!triggers.includes(rule.id)) {
130
+ triggers.push(rule.id)
131
+ assessedFrom.push({ source: 'file_path', pattern: rule.desc, matchedFiles: [file] })
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // 基于 diff 内容(P0 关键词)
138
+ const diffText = diffContent || combined
139
+ for (const rule of P0_CONTENT_PATTERNS) {
140
+ if (rule.pattern.test(diffText)) {
141
+ rule.pattern.lastIndex = 0
142
+ if (!triggers.includes(rule.id)) {
143
+ triggers.push(rule.id)
144
+ assessedFrom.push({ source: 'content', pattern: rule.desc })
145
+ }
146
+ }
147
+ }
148
+
149
+ // protected files
150
+ for (const file of changedFiles) {
151
+ if (PROTECTED_FILE_NAMES.some(p => file === p || file.endsWith('/' + p))) {
152
+ const id = 'R-005'
153
+ if (!triggers.includes(id)) {
154
+ triggers.push(id)
155
+ assessedFrom.push({ source: 'protected_file', pattern: file })
156
+ }
157
+ }
158
+ // 用户自定义 protected files
159
+ if (protectedFiles.includes(file)) {
160
+ const id = 'R-005'
161
+ if (!triggers.includes(id)) {
162
+ triggers.push(id)
163
+ assessedFrom.push({ source: 'protected_file', pattern: file })
164
+ }
165
+ }
166
+ }
167
+
168
+ // next-action.json 有 blocking questions
169
+ if (nextAction && nextAction.has_blocking_questions === true) {
170
+ const id = 'R-009'
171
+ if (!triggers.includes(id)) {
172
+ triggers.push(id)
173
+ assessedFrom.push({ source: 'brainstorm', pattern: 'has_blocking_questions === true' })
174
+ }
175
+ }
176
+
177
+ // ── P1 检测(只在无 P0 时检测)──
178
+ if (!triggers.some(t => t.startsWith('R-00'))) {
179
+ // 基于文件路径
180
+ for (const rule of P1_FILE_PATTERNS) {
181
+ for (const file of changedFiles) {
182
+ if (rule.pattern.test(file)) {
183
+ rule.pattern.lastIndex = 0
184
+ if (!triggers.includes(rule.id)) {
185
+ triggers.push(rule.id)
186
+ assessedFrom.push({ source: 'file_path', pattern: rule.desc, matchedFiles: [file] })
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // 基于内容关键词
193
+ for (const rule of P1_CONTENT_PATTERNS) {
194
+ if (rule.pattern.test(combined)) {
195
+ rule.pattern.lastIndex = 0
196
+ if (!triggers.includes(rule.id)) {
197
+ triggers.push(rule.id)
198
+ assessedFrom.push({ source: 'content', pattern: rule.desc })
199
+ }
200
+ }
201
+ }
202
+
203
+ // 跨模块检测(> 3 个文件的变更)
204
+ if (changedFiles.length > 10) {
205
+ const id = 'R-101'
206
+ if (!triggers.includes(id)) {
207
+ triggers.push(id)
208
+ assessedFrom.push({ source: 'file_count', pattern: `${changedFiles.length} files changed` })
209
+ }
210
+ }
211
+
212
+ // next-action.json 有 NEEDS_REVIEW 决策
213
+ if (nextAction && Array.isArray(nextAction.auto_decisions)) {
214
+ for (const d of nextAction.auto_decisions) {
215
+ if (d.status === 'NEEDS_REVIEW') {
216
+ const id = 'R-102'
217
+ if (!triggers.includes(id)) {
218
+ triggers.push(id)
219
+ assessedFrom.push({ source: 'brainstorm', pattern: `NEEDS_REVIEW: ${d.decision}` })
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ // ── 判定等级 ──
227
+ let level = 'P2'
228
+ if (triggers.some(t => t.match(/^R-0\d+$/))) {
229
+ level = 'P0'
230
+ } else if (triggers.some(t => t.match(/^R-1\d+$/))) {
231
+ level = 'P1'
232
+ }
233
+
234
+ // ── apply 决策 ──
235
+ const applyBlockers = []
236
+ if (level === 'P0') {
237
+ applyBlockers.push('风险等级 P0:需要用户确认后才能 apply')
238
+ }
239
+
240
+ const canAutoApply = level !== 'P0' && applyBlockers.length === 0
241
+ const applyReason = canAutoApply
242
+ ? `风险等级 ${level},无 P0 触发,无 protected files 修改`
243
+ : applyBlockers.join('; ')
244
+
245
+ return { level, triggers, assessedFrom, applyBlockers, canAutoApply, applyReason }
246
+ }
247
+
248
+ // ============ 向后兼容 ============
249
+
250
+ /**
251
+ * 旧的 detectChangeRisk 接口保持向后兼容
64
252
  * @returns {{ level: string, triggers: string[], requiredVerification: string[] }}
65
253
  */
66
254
  export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
@@ -114,9 +302,6 @@ export function detectChangeRisk({ designContent = '', planContent = '', changed
114
302
 
115
303
  /**
116
304
  * 检查 verify-result.md 是否包含集成验证证据
117
- * @param {string} verifyContent
118
- * @param {string[]} requiredVerification
119
- * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
120
305
  */
121
306
  export function checkIntegrationEvidence(verifyContent, requiredVerification) {
122
307
  const errors = []
@@ -0,0 +1,73 @@
1
+ /**
2
+ * classify-change.js — 变更规模分类器
3
+ *
4
+ * 将用户需求描述分类为 quick / auto / full,
5
+ * 供 auto 模式决定内部流程深度。
6
+ */
7
+
8
+ /**
9
+ * 用户显式指定的关键词 → 强制模式
10
+ */
11
+ const FORCE_FULL_PATTERNS = [
12
+ /数据库|database|schema/i,
13
+ /迁移|migration|migrate/i,
14
+ /鉴权|权限|auth|permission|rbac/i,
15
+ /支付|payment|billing/i,
16
+ /重构|refactor.*architectur/i,
17
+ /微服务|microserv/i,
18
+ ]
19
+
20
+ const FORCE_QUICK_PATTERNS = [
21
+ /fix typo/i,
22
+ /更新文案|改文案|文案修改/i,
23
+ /样式调整|style.*tweak/i,
24
+ /修复.*\s*\bbug\b.*\bfix\b/i,
25
+ ]
26
+
27
+ /**
28
+ * 分类变更规模
29
+ * @param {object} opts
30
+ * @param {string} opts.description - 用户需求描述
31
+ * @param {string} [opts.explicitMode] - 用户显式指定的模式(auto/quick/full)
32
+ * @param {object} [opts.localConfig] - local.yaml 中的 auto_mode 配置
33
+ * @returns {{ mode: 'quick'|'auto'|'full', reason: string }}
34
+ */
35
+ export function classifyChange({ description = '', explicitMode, localConfig } = {}) {
36
+ // 1. 用户显式指定优先级最高
37
+ if (explicitMode && ['quick', 'auto', 'full'].includes(explicitMode)) {
38
+ return { mode: explicitMode, reason: '用户显式指定' }
39
+ }
40
+
41
+ // 2. local.yaml force patterns
42
+ if (localConfig) {
43
+ const forceFullPatterns = localConfig.force_full_patterns || []
44
+ const forceQuickPatterns = localConfig.force_quick_patterns || []
45
+
46
+ for (const pattern of forceFullPatterns) {
47
+ if (new RegExp(pattern, 'i').test(description)) {
48
+ return { mode: 'full', reason: `local.yaml force_full_pattern 匹配: ${pattern}` }
49
+ }
50
+ }
51
+ for (const pattern of forceQuickPatterns) {
52
+ if (new RegExp(pattern, 'i').test(description)) {
53
+ return { mode: 'quick', reason: `local.yaml force_quick_pattern 匹配: ${pattern}` }
54
+ }
55
+ }
56
+ }
57
+
58
+ // 3. 默认关键词匹配
59
+ for (const pattern of FORCE_QUICK_PATTERNS) {
60
+ if (pattern.test(description)) {
61
+ return { mode: 'quick', reason: `命中 quick 关键词: ${pattern.source}` }
62
+ }
63
+ }
64
+
65
+ for (const pattern of FORCE_FULL_PATTERNS) {
66
+ if (pattern.test(description)) {
67
+ return { mode: 'full', reason: `命中 full 关键词: ${pattern.source}` }
68
+ }
69
+ }
70
+
71
+ // 4. 默认 auto
72
+ return { mode: 'auto', reason: '默认模式' }
73
+ }
package/src/run.js CHANGED
@@ -113,6 +113,9 @@ import { checkTransition, runValidators } from './stage-contract.js'
113
113
  import { buildExecuteSteps } from './stages/execute.js'
114
114
  import { buildPlanSteps } from './stages/plan.js'
115
115
  import { formatExecuteSummary } from './worktree-apply.js'
116
+ import { classifyChange } from './classify-change.js'
117
+ import { detectRiskProfile } from './change-risk-profile.js'
118
+ import { definition as brainstormAutoDef } from './stages/brainstorm-auto.js'
116
119
 
117
120
  /**
118
121
  * 从 _module-map.yaml 读取模块上下文索引
@@ -772,19 +775,19 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
772
775
  }
773
776
  }
774
777
 
775
- // Execute: 注入 currentExecuteRunId(从 runtime 标记文件读取)
778
+ // Execute: 注入 currentExecuteRunId(从变更专属标记文件读取)
776
779
  if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
777
- const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
778
- const runIdFile = join(execSpecBase, '.runtime', 'current-execute-run-id')
779
780
  let runId = ''
781
+ const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
782
+ const runIdFile = join(execSpecBase, '.runtime', `current-execute-run-id-${effectiveChange}`)
780
783
  try {
781
784
  if (existsSync(runIdFile)) {
782
785
  runId = readFileSync(runIdFile, 'utf8').trim()
783
786
  }
784
787
  } catch {}
785
788
  if (!runId) {
786
- const { generateExecuteRunId, getLatestExecuteRunId } = await import('./task-review.js')
787
- runId = getLatestExecuteRunId(join(execSpecBase, '.runtime')) || generateExecuteRunId()
789
+ const { generateExecuteRunId } = await import('./task-review.js')
790
+ runId = generateExecuteRunId()
788
791
  }
789
792
  promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
790
793
  }
@@ -1283,7 +1286,7 @@ export async function runCommand(args, cwd, specDir = null) {
1283
1286
  '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
1284
1287
  '--files', '--allow-new', '--force-baseline', '--force-rescan',
1285
1288
  '--json', '--dir', '--help',
1286
- '--reopen', '--from-step',
1289
+ '--reopen', '--from-step', '--mode',
1287
1290
  ])
1288
1291
  for (let i = 0; i < flags.length; i++) {
1289
1292
  const f = flags[i]
@@ -1340,7 +1343,7 @@ export async function runCommand(args, cwd, specDir = null) {
1340
1343
 
1341
1344
  // -- auto 模式:自动推进所有流程阶段
1342
1345
  if (stageName === 'auto') {
1343
- return await runAutoMode(pm, progress, cwd, flags, effectiveChange)
1346
+ return await runAutoMode(pm, progress, cwd, flags, effectiveChange, platformOpts)
1344
1347
  }
1345
1348
 
1346
1349
  // --change 只作为变更名标识,不再拦截流程
@@ -1528,16 +1531,24 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
1528
1531
  }
1529
1532
  }
1530
1533
 
1531
- // ── execute 阶段启动时固定 executeRunId ──
1534
+ // ── execute 阶段启动时固定 executeRunId(绑定变更名,避免跨变更复用) ──
1532
1535
  let currentExecuteRunId = null
1533
1536
  if (stageName === 'execute') {
1534
- const { generateExecuteRunId, getLatestExecuteRunId } = await import('./task-review.js')
1537
+ const { generateExecuteRunId } = await import('./task-review.js')
1535
1538
  const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
1536
1539
  const runtimeRoot = join(execSpecBase, '.runtime')
1537
- currentExecuteRunId = getLatestExecuteRunId(runtimeRoot) || generateExecuteRunId()
1538
- // 写入 runtime 标记文件,确保整个 execute 生命周期内 runId 不变
1540
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1539
1541
  mkdirSync(runtimeRoot, { recursive: true })
1540
- writeFileSync(join(runtimeRoot, 'current-execute-run-id'), currentExecuteRunId + '\n')
1542
+ // 优先读取已有的变更专属标记文件
1543
+ try {
1544
+ if (existsSync(runIdFile)) {
1545
+ currentExecuteRunId = readFileSync(runIdFile, 'utf8').trim()
1546
+ }
1547
+ } catch {}
1548
+ if (!currentExecuteRunId) {
1549
+ currentExecuteRunId = generateExecuteRunId()
1550
+ writeFileSync(runIdFile, currentExecuteRunId + '\n')
1551
+ }
1541
1552
  }
1542
1553
 
1543
1554
  // 自动探测 currentChange
@@ -2202,7 +2213,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2202
2213
  // plan 阶段 "generate_plan" 完成后,动态插入任务蓝图 + postcheck 步骤
2203
2214
  // 使用稳定 id 匹配,不依赖中文标题
2204
2215
  if (stageName === 'plan') {
2205
- const currentStepDef = defSteps?.[currentIdx]
2216
+ const currentStepDef = defStepsForCurrent?.[currentIdx]
2206
2217
  const currentStepEntry = steps[currentIdx]
2207
2218
  const stepId = currentStepDef?.id || currentStepEntry?.id || currentStepEntry?._stepId
2208
2219
  if (stepId === 'generate_plan') {
@@ -2667,7 +2678,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2667
2678
  // ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
2668
2679
  if (stageName === 'execute') {
2669
2680
  try {
2670
- const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence, getLatestExecuteRunId, generateExecuteRunId } = await import('./task-review.js')
2681
+ const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('./task-review.js')
2671
2682
  const effectiveSpecBase = platformOpts?.specRoot || specBase
2672
2683
  const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
2673
2684
  const planPath = planFile ? join(planFile, 'plan.md') : null
@@ -2676,9 +2687,16 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2676
2687
  const planContent = readFileSync(planPath, 'utf8')
2677
2688
  const runtimeRoot = join(effectiveSpecBase, '.runtime')
2678
2689
 
2679
- // 找到 execute run id:优先从 runtime 目录找最新,否则生成新的
2680
- let executeRunId = getLatestExecuteRunId(runtimeRoot)
2690
+ // execute run id:从变更专属标记文件读取
2691
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
2692
+ let executeRunId = ''
2693
+ try {
2694
+ if (existsSync(runIdFile)) {
2695
+ executeRunId = readFileSync(runIdFile, 'utf8').trim()
2696
+ }
2697
+ } catch {}
2681
2698
  if (!executeRunId) {
2699
+ const { generateExecuteRunId } = await import('./task-review.js')
2682
2700
  executeRunId = generateExecuteRunId()
2683
2701
  }
2684
2702
 
@@ -3016,14 +3034,28 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
3016
3034
  /**
3017
3035
  * auto 模式:自动推进 brainstorm → plan → execute → verify
3018
3036
  */
3019
- async function runAutoMode(pm, progress, cwd, flags, changeName) {
3020
- const flowStages = ['brainstorm', 'plan', 'execute', 'verify']
3037
+ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts = {}) {
3038
+ const flowStages = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
3021
3039
  const isDone = flags.includes('--done')
3022
3040
  const outputIdx = flags.indexOf('--output')
3023
3041
  const outputText = outputIdx !== -1 && flags[outputIdx + 1] ? flags[outputIdx + 1] : null
3024
3042
  const inputIdx = flags.indexOf('--input')
3025
3043
  const inputText = inputIdx !== -1 && flags[inputIdx + 1] ? flags[inputIdx + 1] : null
3026
3044
  const skipApproval = flags.includes('--skip-approval')
3045
+ const explicitMode = (() => {
3046
+ const m = flags.indexOf('--mode')
3047
+ return m !== -1 && flags[m + 1] ? flags[m + 1] : null
3048
+ })()
3049
+ const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
3050
+
3051
+ // Helper: 在 auto 模式下获取步骤定义
3052
+ const getAutoSteps = async (stage) => {
3053
+ if (stage === 'brainstorm') {
3054
+ return brainstormAutoDef.steps
3055
+ }
3056
+ return getStageSteps(stage, cwd, progress, platformOpts?.specRoot || null)
3057
+ }
3058
+
3027
3059
  const nextInFlow = (stage) => {
3028
3060
  const i = flowStages.indexOf(stage)
3029
3061
  return i >= 0 && i < flowStages.length - 1 ? flowStages[i + 1] : null
@@ -3032,6 +3064,24 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3032
3064
  const ensureAutoStage = async (stage) => {
3033
3065
  const stageChanged = progress.currentStage !== stage
3034
3066
  progress.currentStage = stage
3067
+ // Auto 模式下 brainstorm 使用 artifact-first 步骤
3068
+ if (stage === 'brainstorm') {
3069
+ const existingSteps = progress.stages?.brainstorm?.steps
3070
+ const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '状态检查与上下文加载'
3071
+ if (!isAutoModeSteps) {
3072
+ if (!progress.stages) progress.stages = {}
3073
+ progress.stages.brainstorm = {
3074
+ status: 'in-progress',
3075
+ startedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
3076
+ completedAt: null,
3077
+ steps: brainstormAutoDef.steps.map(s => ({ name: s.name, status: 'pending' }))
3078
+ }
3079
+ await pm._write(cwd, progress, changeName)
3080
+ triggerSync(cwd, changeName, platformOpts)
3081
+ progress = await pm.read(cwd, changeName)
3082
+ return progress
3083
+ }
3084
+ }
3035
3085
  const changed = await ensureStageSteps(progress, stage, cwd)
3036
3086
  if (stageChanged || changed) {
3037
3087
  await pm._write(cwd, progress, changeName)
@@ -3041,6 +3091,18 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3041
3091
  return progress
3042
3092
  }
3043
3093
 
3094
+ // ── Classify change on first entry ──
3095
+ if (!progress.stages?.brainstorm?.status && !progress.stages?.plan?.status) {
3096
+ const { classifyChange } = await import('./classify-change.js')
3097
+ const classification = classifyChange({ description: inputText || '', explicitMode })
3098
+ if (classification.mode === 'quick') {
3099
+ console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
3100
+ console.log(` 此变更建议使用 quick 模式,运行:sillyspec run quick "${inputText || '需求'}"`)
3101
+ return
3102
+ }
3103
+ console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
3104
+ }
3105
+
3044
3106
  let currentStage = progress.currentStage
3045
3107
  if (!currentStage || progress.stages?.[currentStage]?.status === 'completed') {
3046
3108
  currentStage = firstOpenStage()
@@ -3076,7 +3138,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3076
3138
  }
3077
3139
  console.log('')
3078
3140
 
3079
- const defSteps = await getStageSteps(currentStage, cwd, progress)
3141
+ const defSteps = await getAutoSteps(currentStage)
3080
3142
  const pendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
3081
3143
  if (pendingIdx === -1) {
3082
3144
  const wsIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'waiting') ?? -1
@@ -3121,7 +3183,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3121
3183
 
3122
3184
  const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
3123
3185
  if (nextPendingIdx !== -1) {
3124
- const defSteps = await getStageSteps(currentStage, cwd, progress)
3186
+ const defSteps = await getAutoSteps(currentStage)
3125
3187
  // execute 阶段启动前检查审批
3126
3188
  if (currentStage === 'execute' && !skipApproval) {
3127
3189
  const approval = await checkApproval(cwd, changeName, platformOpts)
@@ -3146,6 +3208,31 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3146
3208
  return
3147
3209
  }
3148
3210
 
3211
+ // ── next-action.json 驱动:brainstorm → plan 推进判断 ──
3212
+ if (currentStage === 'brainstorm' && next === 'plan') {
3213
+ const changeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot || null)
3214
+ if (changeDir) {
3215
+ const nextActionFile = join(changeDir, 'brainstorm', 'next-action.json')
3216
+ try {
3217
+ const nextAction = JSON.parse(readFileSync(nextActionFile, 'utf8'))
3218
+ if (nextAction.has_blocking_questions === true) {
3219
+ console.log(`\n⏸️ brainstorm 有阻塞问题,无法自动进入 plan:`)
3220
+ for (const q of (nextAction.questions || [])) {
3221
+ console.log(` Q-${q.id}: ${q.question}`)
3222
+ if (q.options) console.log(` 选项:${q.options.join(' / ')}`)
3223
+ if (q.recommended) console.log(` 推荐:${q.recommended}`)
3224
+ }
3225
+ console.log(`\n 请回答阻塞问题后继续:sillyspec run auto --done --output "已回答"`)
3226
+ return
3227
+ }
3228
+ console.log(`\n✅ next-action.json: ${nextAction.status},自动进入 plan`)
3229
+ } catch (e) {
3230
+ // next-action.json 不存在或格式错误,继续推进(向后兼容)
3231
+ console.log(`\n⚠️ next-action.json 未找到,继续进入 plan`)
3232
+ }
3233
+ }
3234
+ }
3235
+
3149
3236
  progress.currentStage = next
3150
3237
  if (!progress.stages[next]) {
3151
3238
  progress.stages[next] = { status: 'pending', steps: [], startedAt: null, completedAt: null }
@@ -3161,7 +3248,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3161
3248
  progress = await pm.read(cwd, changeName)
3162
3249
 
3163
3250
  console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
3164
- const nextSteps = await getStageSteps(next, cwd, progress)
3251
+ const nextSteps = await getAutoSteps(next)
3165
3252
  const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
3166
3253
  if (firstPending !== -1) {
3167
3254
  // execute 阶段启动前检查审批
@@ -0,0 +1,229 @@
1
+ /**
2
+ * brainstorm-auto.js — auto/full 模式使用的 brainstorm 步骤定义
3
+ *
4
+ * 与 brainstorm.js 的区别:
5
+ * 1. artifact-first:直接写文件,对话只输出摘要
6
+ * 2. 自动决策:基于 AC-001~AC-010 checklist,不频繁请示
7
+ * 3. next-action.json:结构化产物,驱动下游推进
8
+ * 4. 只有 blocking questions 才 wait 用户
9
+ * 5. 步骤从 ~13 步精简为 4 步
10
+ */
11
+
12
+ export const definition = {
13
+ name: 'brainstorm',
14
+ title: '头脑风暴(自动模式)',
15
+ description: '探索需求、分析技术方案、识别风险 — artifact-first,自动决策优先',
16
+
17
+ steps: [
18
+ {
19
+ name: '状态检查与上下文加载',
20
+ prompt: `检查状态、加载项目上下文、匹配模块。
21
+
22
+ ### 操作
23
+ 1. 运行 \`sillyspec progress show\`,确认 currentStage 为 "brainstorm"
24
+ 2. 如果未初始化,提示先运行 sillyspec init
25
+ 3. **检查变更名称**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change\`),直接重命名为有意义名称,然后运行 \`sillyspec change-rename <旧名> <新名>\`
26
+
27
+ ### 加载上下文
28
+ 4. 读取 CODEBASE-OVERVIEW.md + 共享规范 + 子项目上下文
29
+ 5. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
30
+ 6. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`
31
+ 7. 棕地项目:读取 scan 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
32
+ 8. 加载模块索引:读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(如存在)
33
+ 9. 查看进行中的变更:\`ls .sillyspec/changes/ | grep -v archive\`
34
+ 10. 检查同名变更或可复用模板
35
+
36
+ ### 模块匹配
37
+ 根据用户描述的需求关键词,匹配相关模块(用 _module-map.yaml 的 tags/aliases/paths)。
38
+
39
+ ### 子项目判定
40
+ - 单项目:直接确认
41
+ - 多项目且用户已指定:直接确认
42
+ - 多项目且用户未指定:列出项目列表,需要用户确认
43
+
44
+ ### 如果有用户提供的原型/截图
45
+ 分析提取:页面结构、表单字段、交互流程、业务规则。
46
+
47
+ ### 输出
48
+ 状态摘要 + 项目现状理解(3-5 句)+ 涉及模块列表 + 子项目 + 原型分析(如有)
49
+
50
+ ### 注意
51
+ - 以 CLI 返回为准,不要自行推断阶段
52
+ - 不要用 mv 命令重命名变更目录,必须用 \`sillyspec change-rename\``,
53
+ outputHint: '状态摘要 + 上下文 + 模块匹配',
54
+ optional: false
55
+ },
56
+ {
57
+ name: '需求分析与方案设计',
58
+ requiresWait: true,
59
+ repeatableWait: true,
60
+ maxWaitRounds: 5,
61
+ waitReason: '等待用户回答需求问题',
62
+ waitOptions: ['回答见--answer', '信息够了,进入设计'],
63
+ prompt: `分析需求,必要时追问,然后设计方案。
64
+
65
+ ### 操作
66
+
67
+ #### A. 需求评估
68
+ 1. 汇总上一步加载的上下文和用户需求
69
+ 2. 判断需求是否清晰:
70
+ - 目标明确、范围清楚、无歧义 → 跳过追问,直接进入方案设计
71
+ - 有歧义或不清楚 → 挑**一个最关键的**问题追问
72
+
73
+ #### B. 追问规则(只在需要时)
74
+ - 一次只问一个问题,不要一次性列出多个问题
75
+ - 能从代码/文档确认的不要问用户
76
+ - 多选题优于开放式问题
77
+ - YAGNI — 砍掉不需要的功能
78
+ - 2-3 轮问答就应进入方案设计
79
+ - 优先追问影响架构/数据/接口的问题,实现细节不要问
80
+
81
+ **如果需要追问:**调用 \`sillyspec run brainstorm --wait --reason "等待用户回答需求问题" --options "回答见--answer,信息够了,进入设计" --output "你的单个问题"\`
82
+
83
+ #### C. 复杂度评估(追问结束后或不需要追问时)
84
+ 1. 判断是否需要拆分或走批量模式:
85
+ - 3+ 个可独立交付的功能模块 → 建议拆分
86
+ - 任务 > 10 且有重复模式 → 建议批量模式
87
+ - 简单 CRUD → 不拆
88
+ 2. 如果需要拆分/批量模式:暂停等用户确认
89
+ - 调用:\`sillyspec run brainstorm --wait --reason "等待用户确认拆分方案" --options "同意拆分,不需要拆分,走批量模式" --output "拆分方案摘要"\`
90
+ 3. 不需要拆分 → 继续
91
+
92
+ #### D. 方案设计(自动决策优先)
93
+ 1. 基于需求理解和上下文,提出 1-3 种实现方案
94
+ 2. **使用自动决策 checklist(AC-001~AC-010)判断是否需要用户选择:**
95
+
96
+ \`\`\`
97
+ AC-001: 未修改公共 API
98
+ AC-002: 未修改数据库 schema
99
+ AC-003: 未涉及鉴权/权限
100
+ AC-004: 未扩大 allowed_paths
101
+ AC-005: 未引入新外部依赖
102
+ AC-006: 未修改核心模块(workflow/daemon/session/lifecycle)
103
+ AC-007: 已有项目约定可复用
104
+ AC-008: 不影响向后兼容
105
+ AC-009: 不涉及数据迁移
106
+ AC-010: 单模块范围内可完成
107
+ \`\`\`
108
+
109
+ 3. **自动决策判定:**
110
+ - 只有一个明显合理方案 + checklist 全部 ✅ → **自动选择,标记 AUTO_DECIDED**
111
+ - 有多个方案但影响不大(checklist 全部 ✅)→ **自动选推荐方案,标记 AUTO_DECIDED**
112
+ - 有多个方案且 checklist 有 ❌(影响架构/数据/接口/权限/兼容性)→ **只有这一种情况才暂停等用户选择**
113
+
114
+ 4. 如果需要用户选择方案:
115
+ - 调用:\`sillyspec run brainstorm --wait --reason "等待用户选择方案" --options "方案A,方案B,方案C" --output "方案对比摘要"\`
116
+
117
+ #### E. Design Grill(轻量交叉审查)
118
+ 对方案做快速交叉审查:
119
+ - 检查方案与 ARCHITECTURE.md / CONVENTIONS.md 的一致性
120
+ - 检查是否有术语歧义、边界遗漏
121
+ - 能自动解决的直接修正,只有需要业务判断的才问用户
122
+ - 跳过条件:单模块、无状态流转、< 3 个文件变更
123
+
124
+ ### 输出
125
+ 需求理解摘要 + 复杂度评估 + 方案决策(AUTO_DECIDED 或用户选择)
126
+
127
+ ### 注意
128
+ - **不要自问自答。** 不要在自己输出中模拟用户回答然后说"需求已明确"
129
+ - checklist 判定结果必须在 decisions.md 中记录依据`,
130
+ outputHint: '需求理解 + 方案决策',
131
+ optional: false
132
+ },
133
+ {
134
+ name: '生成设计产物',
135
+ prompt: `将设计方案写入文件(artifact-first),不回显正文。
136
+
137
+ ### 操作
138
+ 1. 确保变更目录存在:\`mkdir -p .sillyspec/changes/<change-name>/brainstorm\`
139
+ 2. **直接将设计方案写入文件,不要先在对话中输出完整内容再写文件。**
140
+
141
+ ### 产物文件
142
+
143
+ #### design.md(必填,写入 \`brainstorm/design.md\`)
144
+ 包含:背景、设计目标、非目标、总体方案、文件变更清单、接口定义、数据模型(如涉及)、兼容策略(brownfield)、风险登记、决策追踪。
145
+
146
+ #### decisions.md(必填,写入 \`brainstorm/decisions.md\`)
147
+ 记录所有决策:
148
+ \`\`\`markdown
149
+ ## D-001@v1: 决策短标题
150
+ - type: architecture | boundary | compatibility | ...
151
+ - priority: P0 | P1 | P2
152
+ - status: AUTO_DECIDED | NEEDS_REVIEW | USER_DECIDED
153
+ - source: user | code | docs
154
+ - question: 被解决的问题
155
+ - answer: 选择或结论
156
+ - checklist: AC-001 ✅, AC-007 ✅(AUTO_DECIDED 时必须列出)
157
+ - normalized_requirement: 可测试约束
158
+ - impacts: [FR-01, task-01]
159
+ - evidence: 代码/文档路径或用户回答轮次
160
+ \`\`\`
161
+
162
+ #### gaps.md(必填,写入 \`brainstorm/gaps.md\`)
163
+ 记录已识别的缺口。status=BLOCKER 的缺口会在 plan-postcheck 中触发回退。
164
+
165
+ #### assumptions.md(必填,写入 \`brainstorm/assumptions.md\`)
166
+ 记录隐含假设和验证方法。
167
+
168
+ #### next-action.json(必填,写入 \`brainstorm/next-action.json\`)
169
+ \`\`\`json
170
+ {
171
+ "status": "ready_for_plan" | "waiting_for_user",
172
+ "decision_level": "high" | "medium" | "low",
173
+ "has_blocking_questions": true | false,
174
+ "blocking_reasons": [],
175
+ "questions": [],
176
+ "auto_decisions": [
177
+ { "id": "D-001", "decision": "方案描述", "reason": "AC-007 ✅" }
178
+ ]
179
+ }
180
+ \`\`\`
181
+
182
+ ### 自审
183
+ 对 design.md 执行自审(需求覆盖、文件变更清单具体性、验收标准可测试、非目标清晰、兼容策略)。发现问题直接修改文件。
184
+
185
+ ### 如果有 blocking questions
186
+ 将问题写入 next-action.json.questions,在对话中输出问题列表和推荐选项,然后 --wait。
187
+
188
+ ### 输出(artifact-first)
189
+ 只输出摘要:
190
+ \`\`\`
191
+ 已生成设计产物(5 个文件):
192
+ - brainstorm/design.md — 采用 xxx 方案,涉及 N 个文件变更
193
+ - brainstorm/decisions.md — M 个决策,K 个 AUTO_DECIDED
194
+ - brainstorm/gaps.md — J 个缺口,0 个 BLOCKER
195
+ - brainstorm/assumptions.md — L 个假设
196
+ - brainstorm/next-action.json — ready_for_plan / waiting_for_user
197
+ \`\`\``,
198
+ outputHint: '产物摘要',
199
+ optional: false
200
+ },
201
+ {
202
+ name: '生成规范文件',
203
+ requiresWait: true,
204
+ waitReason: '等待用户最终确认',
205
+ waitOptions: ['确认', '需要修改', '推翻重来'],
206
+ prompt: `生成 proposal.md / requirements.md / tasks.md,让用户确认。
207
+
208
+ ### 操作
209
+ 1. 基于 brainstorm/ 下的设计产物,生成规范文件(直接写文件):
210
+ - **proposal.md**:动机、关键问题、变更范围、不在范围内、成功标准
211
+ - **requirements.md**:角色表 + FR 编号需求 + Given/When/Then + 非功能需求
212
+ - **tasks.md**:任务列表(只列名称,细节在 plan 阶段展开)
213
+ 2. 如果 brainstorm/decisions.md 有 AUTO_DECIDED 决策,在变更根目录也写一份 decisions.md
214
+ 3. 所有规范文件头部包含 YAML frontmatter
215
+ 4. \`git add .sillyspec/\` — 暂存规范文件(不要 commit)
216
+
217
+ ### 输出(摘要)
218
+ 规范文件路径列表(各一句话说明)
219
+
220
+ ### 铁律
221
+ - **直接写文件,不在对话中输出完整内容**
222
+ - 暂停等待用户确认:\`sillyspec run brainstorm --wait --reason "等待用户最终确认" --options "确认,需要修改,推翻重来" --output "规范文件摘要"\`
223
+ - 禁止自动 commit
224
+ - 禁止在确认前推进到后续阶段`,
225
+ outputHint: '规范文件摘要',
226
+ optional: false
227
+ }
228
+ ]
229
+ }