sillyspec 3.19.2 → 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/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
  }
@@ -1067,6 +1070,19 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
1067
1070
  }
1068
1071
  }
1069
1072
 
1073
+ /**
1074
+ * Plan postcheck 的执行代理:委托给 plan-postcheck.js 模块
1075
+ */
1076
+ async function executePlanPostcheck(cwd, platformOpts) {
1077
+ const { executePlanPostcheck: runPostcheck } = await import('./stages/plan-postcheck.js')
1078
+ const { resolveChangeDir } = await import('./modules.js')
1079
+ await runPostcheck({
1080
+ cwd,
1081
+ specRoot: platformOpts?.specRoot,
1082
+ resolveChangeDir
1083
+ })
1084
+ }
1085
+
1070
1086
  /**
1071
1087
  * sillyspec run <stage> 主命令
1072
1088
  */
@@ -1087,6 +1103,19 @@ export async function runCommand(args, cwd, specDir = null) {
1087
1103
  process.exit(1)
1088
1104
  }
1089
1105
 
1106
+ // ── cwd 纠正:向上查找真实项目根 ──
1107
+ // 防止多 project 工作区中 cwd 停在子目录(如 backend/)时
1108
+ // 状态写入子目录下误建的 .sillyspec,导致状态分裂
1109
+ if (!specDir) {
1110
+ const resolvedRoot = resolveSpecDir(cwd)
1111
+ if (resolvedRoot && resolvedRoot !== join(cwd, '.sillyspec')) {
1112
+ const realRoot = dirname(resolvedRoot)
1113
+ if (realRoot !== cwd) {
1114
+ cwd = realRoot
1115
+ }
1116
+ }
1117
+ }
1118
+
1090
1119
  // 平台模式参数(供 SillyHub 等平台调用)
1091
1120
  // --spec-dir 是统一参数名,--spec-root 保留为向后兼容别名
1092
1121
  const getFlagValue = (name) => {
@@ -1257,7 +1286,7 @@ export async function runCommand(args, cwd, specDir = null) {
1257
1286
  '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
1258
1287
  '--files', '--allow-new', '--force-baseline', '--force-rescan',
1259
1288
  '--json', '--dir', '--help',
1260
- '--reopen', '--from-step',
1289
+ '--reopen', '--from-step', '--mode',
1261
1290
  ])
1262
1291
  for (let i = 0; i < flags.length; i++) {
1263
1292
  const f = flags[i]
@@ -1314,7 +1343,7 @@ export async function runCommand(args, cwd, specDir = null) {
1314
1343
 
1315
1344
  // -- auto 模式:自动推进所有流程阶段
1316
1345
  if (stageName === 'auto') {
1317
- return await runAutoMode(pm, progress, cwd, flags, effectiveChange)
1346
+ return await runAutoMode(pm, progress, cwd, flags, effectiveChange, platformOpts)
1318
1347
  }
1319
1348
 
1320
1349
  // --change 只作为变更名标识,不再拦截流程
@@ -1502,16 +1531,24 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
1502
1531
  }
1503
1532
  }
1504
1533
 
1505
- // ── execute 阶段启动时固定 executeRunId ──
1534
+ // ── execute 阶段启动时固定 executeRunId(绑定变更名,避免跨变更复用) ──
1506
1535
  let currentExecuteRunId = null
1507
1536
  if (stageName === 'execute') {
1508
- const { generateExecuteRunId, getLatestExecuteRunId } = await import('./task-review.js')
1537
+ const { generateExecuteRunId } = await import('./task-review.js')
1509
1538
  const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
1510
1539
  const runtimeRoot = join(execSpecBase, '.runtime')
1511
- currentExecuteRunId = getLatestExecuteRunId(runtimeRoot) || generateExecuteRunId()
1512
- // 写入 runtime 标记文件,确保整个 execute 生命周期内 runId 不变
1540
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1513
1541
  mkdirSync(runtimeRoot, { recursive: true })
1514
- 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
+ }
1515
1552
  }
1516
1553
 
1517
1554
  // 自动探测 currentChange
@@ -1686,6 +1723,8 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
1686
1723
  await executeScanPreflight(cwd, platformOpts, scanProfile)
1687
1724
  } else if (cliAction === 'scanPostcheck') {
1688
1725
  await executeScanPostcheck(cwd, platformOpts, scanProfile)
1726
+ } else if (cliAction === 'planPostcheck') {
1727
+ await executePlanPostcheck(cwd, platformOpts)
1689
1728
  }
1690
1729
  stageData.steps[currentIdx].status = 'completed'
1691
1730
  stageData.steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
@@ -2171,29 +2210,43 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2171
2210
  }
2172
2211
  }
2173
2212
 
2174
- // plan 阶段 "展开任务" 完成后,动态插入任务蓝图协调器步骤
2175
- if (stageName === 'plan' && steps[currentIdx]?.name === '展开任务并分组') {
2176
- const changeDir = resolveChangeDir(cwd, progress)
2177
- if (changeDir) {
2178
- const planFile = join(changeDir, 'plan.md')
2179
- if (existsSync(planFile)) {
2180
- const planContent = readFileSync(planFile, 'utf8')
2181
- const { buildPlanSteps, fixedPrefix, fixedSuffix } = await import('./stages/plan.js')
2182
- const fullSteps = buildPlanSteps(changeDir, planContent)
2183
- const prefixLen = fixedPrefix.length
2184
- const suffixLen = fixedSuffix.length
2185
- const coordinatorSteps = fullSteps.slice(prefixLen, suffixLen > 0 ? -suffixLen : undefined)
2186
- if (coordinatorSteps.length > 0) {
2187
- for (let i = 0; i < coordinatorSteps.length; i++) {
2188
- steps.splice(currentIdx + 1 + i, 0, {
2189
- name: coordinatorSteps[i].name,
2190
- status: 'pending',
2191
- prompt: coordinatorSteps[i].prompt,
2192
- outputHint: coordinatorSteps[i].outputHint,
2193
- optional: coordinatorSteps[i].optional
2194
- })
2213
+ // plan 阶段 "generate_plan" 完成后,动态插入任务蓝图 + postcheck 步骤
2214
+ // 使用稳定 id 匹配,不依赖中文标题
2215
+ if (stageName === 'plan') {
2216
+ const currentStepDef = defStepsForCurrent?.[currentIdx]
2217
+ const currentStepEntry = steps[currentIdx]
2218
+ const stepId = currentStepDef?.id || currentStepEntry?.id || currentStepEntry?._stepId
2219
+ if (stepId === 'generate_plan') {
2220
+ const changeDir = resolveChangeDir(cwd, progress)
2221
+ if (changeDir) {
2222
+ const planFile = join(changeDir, 'plan.md')
2223
+ if (existsSync(planFile)) {
2224
+ const planContent = readFileSync(planFile, 'utf8')
2225
+ const { buildPlanSteps, fixedPrefix, fixedSuffix } = await import('./stages/plan.js')
2226
+ const fullSteps = buildPlanSteps(changeDir, planContent)
2227
+ const prefixLen = fixedPrefix.length
2228
+ const suffixLen = fixedSuffix.length
2229
+ // 新结构:[...fixedPrefix, coordinatorStep?, postcheckStep?]
2230
+ // fixedSuffix 为空,所以 coordinator + postcheck 都在 prefix 之后
2231
+ const coordinatorSteps = fullSteps.slice(prefixLen, suffixLen > 0 ? -suffixLen : undefined)
2232
+ if (coordinatorSteps.length > 0) {
2233
+ for (let i = 0; i < coordinatorSteps.length; i++) {
2234
+ const stepDef = coordinatorSteps[i]
2235
+ const stepEntry = {
2236
+ id: stepDef.id,
2237
+ name: stepDef.name,
2238
+ status: 'pending',
2239
+ prompt: stepDef.prompt || '',
2240
+ outputHint: stepDef.outputHint,
2241
+ optional: stepDef.optional
2242
+ }
2243
+ // 传递 noAI / _cliAction 属性
2244
+ if (stepDef.noAI) stepEntry.noAI = true
2245
+ if (stepDef._cliAction) stepEntry._cliAction = stepDef._cliAction
2246
+ steps.splice(currentIdx + 1 + i, 0, stepEntry)
2247
+ }
2248
+ console.log(` 📝 已动态插入 ${coordinatorSteps.length} 个步骤(${coordinatorSteps.map(s => s.name).join(', ')})`)
2195
2249
  }
2196
- console.log(` 📝 已动态插入 ${coordinatorSteps.length} 个任务蓝图步骤(${coordinatorSteps.map(s => s.name).join(', ')})`)
2197
2250
  }
2198
2251
  }
2199
2252
  }
@@ -2625,7 +2678,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2625
2678
  // ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
2626
2679
  if (stageName === 'execute') {
2627
2680
  try {
2628
- const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence, getLatestExecuteRunId, generateExecuteRunId } = await import('./task-review.js')
2681
+ const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('./task-review.js')
2629
2682
  const effectiveSpecBase = platformOpts?.specRoot || specBase
2630
2683
  const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
2631
2684
  const planPath = planFile ? join(planFile, 'plan.md') : null
@@ -2634,9 +2687,16 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2634
2687
  const planContent = readFileSync(planPath, 'utf8')
2635
2688
  const runtimeRoot = join(effectiveSpecBase, '.runtime')
2636
2689
 
2637
- // 找到 execute run id:优先从 runtime 目录找最新,否则生成新的
2638
- 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 {}
2639
2698
  if (!executeRunId) {
2699
+ const { generateExecuteRunId } = await import('./task-review.js')
2640
2700
  executeRunId = generateExecuteRunId()
2641
2701
  }
2642
2702
 
@@ -2974,14 +3034,28 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
2974
3034
  /**
2975
3035
  * auto 模式:自动推进 brainstorm → plan → execute → verify
2976
3036
  */
2977
- async function runAutoMode(pm, progress, cwd, flags, changeName) {
2978
- const flowStages = ['brainstorm', 'plan', 'execute', 'verify']
3037
+ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts = {}) {
3038
+ const flowStages = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
2979
3039
  const isDone = flags.includes('--done')
2980
3040
  const outputIdx = flags.indexOf('--output')
2981
3041
  const outputText = outputIdx !== -1 && flags[outputIdx + 1] ? flags[outputIdx + 1] : null
2982
3042
  const inputIdx = flags.indexOf('--input')
2983
3043
  const inputText = inputIdx !== -1 && flags[inputIdx + 1] ? flags[inputIdx + 1] : null
2984
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
+
2985
3059
  const nextInFlow = (stage) => {
2986
3060
  const i = flowStages.indexOf(stage)
2987
3061
  return i >= 0 && i < flowStages.length - 1 ? flowStages[i + 1] : null
@@ -2990,6 +3064,24 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
2990
3064
  const ensureAutoStage = async (stage) => {
2991
3065
  const stageChanged = progress.currentStage !== stage
2992
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
+ }
2993
3085
  const changed = await ensureStageSteps(progress, stage, cwd)
2994
3086
  if (stageChanged || changed) {
2995
3087
  await pm._write(cwd, progress, changeName)
@@ -2999,6 +3091,18 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
2999
3091
  return progress
3000
3092
  }
3001
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
+
3002
3106
  let currentStage = progress.currentStage
3003
3107
  if (!currentStage || progress.stages?.[currentStage]?.status === 'completed') {
3004
3108
  currentStage = firstOpenStage()
@@ -3034,7 +3138,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3034
3138
  }
3035
3139
  console.log('')
3036
3140
 
3037
- const defSteps = await getStageSteps(currentStage, cwd, progress)
3141
+ const defSteps = await getAutoSteps(currentStage)
3038
3142
  const pendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
3039
3143
  if (pendingIdx === -1) {
3040
3144
  const wsIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'waiting') ?? -1
@@ -3079,7 +3183,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3079
3183
 
3080
3184
  const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
3081
3185
  if (nextPendingIdx !== -1) {
3082
- const defSteps = await getStageSteps(currentStage, cwd, progress)
3186
+ const defSteps = await getAutoSteps(currentStage)
3083
3187
  // execute 阶段启动前检查审批
3084
3188
  if (currentStage === 'execute' && !skipApproval) {
3085
3189
  const approval = await checkApproval(cwd, changeName, platformOpts)
@@ -3104,6 +3208,31 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3104
3208
  return
3105
3209
  }
3106
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
+
3107
3236
  progress.currentStage = next
3108
3237
  if (!progress.stages[next]) {
3109
3238
  progress.stages[next] = { status: 'pending', steps: [], startedAt: null, completedAt: null }
@@ -3119,7 +3248,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
3119
3248
  progress = await pm.read(cwd, changeName)
3120
3249
 
3121
3250
  console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
3122
- const nextSteps = await getStageSteps(next, cwd, progress)
3251
+ const nextSteps = await getAutoSteps(next)
3123
3252
  const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
3124
3253
  if (firstPending !== -1) {
3125
3254
  // execute 阶段启动前检查审批
@@ -67,7 +67,12 @@ function buildDecisionRecord(id, body) {
67
67
  const priorityMissing = priorityValue.length === 0
68
68
  const fallbackPriority = (['unresolved', 'blocking'].includes(status) || blocker) ? 'P1' : 'P2'
69
69
  const priority = (priorityValue.match(/P[0-2]/i)?.[0] || fallbackPriority).toUpperCase()
70
- return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing }
70
+ // 解析 supersedes 字段:记录本条决策取代了哪个旧版本
71
+ const supersedesRaw = readDecisionField(body, 'supersedes', '')
72
+ const supersedes = supersedesRaw
73
+ ? supersedesRaw.split(',').map(s => s.trim().toUpperCase().replace(/['"]/g, '')).filter(Boolean)
74
+ : []
75
+ return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing, supersedes }
71
76
  }
72
77
 
73
78
  function findNextDecisionBoundary(content, startIndex) {
@@ -114,8 +119,16 @@ function parseDecisionRecords(content) {
114
119
  function extractCurrentDecisionIds(content) {
115
120
  const records = parseDecisionRecords(content)
116
121
  if (records.length === 0) return extractIds(content, 'D')
122
+ // 收集所有被 supersedes 声明取代的旧版本 ID
123
+ const supersededIds = new Set()
124
+ for (const r of records) {
125
+ for (const oldId of r.supersedes) {
126
+ supersededIds.add(oldId)
127
+ }
128
+ }
117
129
  return records
118
130
  .filter(r => !['superseded', 'rejected'].includes(r.status))
131
+ .filter(r => !supersededIds.has(r.id)) // 被新版本显式取代的旧版本不再校验
119
132
  .map(r => r.id)
120
133
  .sort()
121
134
  }
@@ -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
+ }