sillyspec 3.25.9 → 3.26.0

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.
Files changed (76) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
  3. package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
  4. package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
  8. package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
  9. package/.sillyspec-platform-cleaned +1 -0
  10. package/.sillyspec-platform.json +7 -0
  11. package/README.md +9 -0
  12. package/package.json +2 -2
  13. package/src/change-list.js +11 -8
  14. package/src/change-risk-profile.js +22 -0
  15. package/src/db.js +170 -161
  16. package/src/dispatch/backends/local-agent.js +152 -0
  17. package/src/dispatch/backends/sillyhub-mcp.js +242 -0
  18. package/src/dispatch/probe.js +210 -0
  19. package/src/dispatch/strategy.js +121 -0
  20. package/src/doctor-diagnostics.js +19 -25
  21. package/src/fs-atomic.js +69 -69
  22. package/src/git-helper.js +66 -0
  23. package/src/hooks/worktree-guard.js +64 -55
  24. package/src/index.js +326 -36
  25. package/src/init.js +7 -14
  26. package/src/local-detect.js +120 -110
  27. package/src/machine-interface.js +54 -19
  28. package/src/migrate.js +14 -4
  29. package/src/modules.js +16 -5
  30. package/src/progress/change-registry.js +85 -70
  31. package/src/progress/consistency-doctor.js +71 -14
  32. package/src/progress/shared.js +8 -5
  33. package/src/progress/stage-machine.js +67 -65
  34. package/src/progress/step-store.js +72 -67
  35. package/src/progress.js +448 -236
  36. package/src/quicklog.js +59 -12
  37. package/src/run/command.js +78 -39
  38. package/src/run/complete-handlers.js +1028 -889
  39. package/src/run/complete.js +132 -212
  40. package/src/run/concurrent-detect.js +132 -0
  41. package/src/run/gates.js +302 -19
  42. package/src/run/prompt.js +22 -68
  43. package/src/run/quick-audit.js +10 -9
  44. package/src/run/shared.js +722 -600
  45. package/src/run/stage.js +43 -24
  46. package/src/scan-postcheck.js +9 -31
  47. package/src/sillyhub-mcp/client.js +394 -0
  48. package/src/sillyhub-mcp/config.js +62 -0
  49. package/src/spec-dir-typo.js +71 -0
  50. package/src/stage-contract-engine.js +22 -1
  51. package/src/stage-contract-spec.js +11 -4
  52. package/src/stage-contract.js +26 -3
  53. package/src/stage-review.js +118 -10
  54. package/src/stages/archive.js +4 -6
  55. package/src/stages/brainstorm-auto.js +26 -20
  56. package/src/stages/brainstorm.js +29 -5
  57. package/src/stages/cmd-existence.js +145 -0
  58. package/src/stages/doctor.js +10 -7
  59. package/src/stages/execute.js +64 -22
  60. package/src/stages/plan-postcheck.js +152 -0
  61. package/src/stages/plan.js +16 -31
  62. package/src/stages/quick.js +25 -9
  63. package/src/stages/scan.js +53 -8
  64. package/src/stages/status.js +0 -1
  65. package/src/stages/verify.js +14 -9
  66. package/src/sync.js +433 -16
  67. package/src/task-review.js +59 -1
  68. package/src/verify-postcheck.js +938 -862
  69. package/src/version.js +19 -0
  70. package/src/worktree-apply.js +218 -93
  71. package/src/worktree-deps.js +89 -5
  72. package/src/worktree.js +213 -111
  73. package/templates/prompts/taskcard-rules.md +27 -0
  74. package/templates/prompts/verify-probes.md +3 -3
  75. package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
  76. package/src/stages/propose.js +0 -181
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * run/complete.js(W6 Step7b 从 run.js 抽出)。
3
3
  *
4
- * step 完成处理核心:completeStep(调度主干——WAIT/scanProfile/requiresWait 门控 + 各 handler 调用 +
4
+ * step 完成处理核心:completeStep(调度主干——WAIT/requiresWait 门控 + 各 handler 调用 +
5
5
  * completion path 收尾 + single-step path)+ skipStep/waitStep/continueStep(W6 Step7b-2 续搬)。
6
6
  * 本 commit(7b-1)先搬 completeStep + 2 独占 helper(validateMetadata/validateFileLocations)。
7
7
  *
@@ -15,106 +15,19 @@
15
15
  * - node: join(path) + existsSync/readdirSync/readFileSync/mkdirSync/writeFileSync/appendFileSync/statSync(fs)
16
16
  */
17
17
  import { join } from 'node:path'
18
- import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync } from 'node:fs'
19
- import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions } from './shared.js'
18
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
19
+ import { writeAtomicSync } from '../fs-atomic.js'
20
+ import { withFileLock } from '../quicklog.js'
21
+ import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot } from './shared.js'
20
22
  import { outputStep } from './prompt.js'
21
- import { enforceDepsGate, enforceReviewJsonGate, runStageCompletionGates } from './gates.js'
22
- import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact, handleExecuteWorktreeCleanup, handleScanStageCompleted } from './complete-handlers.js'
23
- import { stageRegistry } from '../stages/index.js'
23
+ import { enforceDepsGate, enforceReviewJsonGate, completeStageGates, readDesignScale } from './gates.js'
24
+ import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
24
25
  import { formatExecuteSummary } from '../worktree-apply.js'
26
+ import { isEndToEndTaskText } from '../change-risk-profile.js'
25
27
 
26
- function validateMetadata(cwd, stageName, specBase) {
27
- const changesDir = join(specBase, 'changes')
28
- if (!existsSync(changesDir)) return
29
-
30
- const cutoff = Date.now() - 10 * 60 * 1000
31
- const missing = []
32
-
33
- function walk(dir) {
34
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
35
- const full = join(dir, entry.name)
36
- try {
37
- if (entry.isDirectory()) { walk(full); continue }
38
- if (!/\.(md|yaml|yml)$/.test(entry.name)) continue
39
- const mtime = statSync(full).mtimeMs
40
- if (mtime < cutoff) continue
41
- const content = readFileSync(full, 'utf-8')
42
- if (!content.includes('author:') && !content.includes('author:')) missing.push(full)
43
- if (!content.includes('created_at:') && !content.includes('created_at:')) missing.push(full)
44
- } catch (e) { /* skip unreadable files */ }
45
- }
46
- }
47
-
48
- walk(changesDir)
49
- const unique = [...new Set(missing)]
50
- if (unique.length > 0) {
51
- console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
52
- unique.forEach(f => console.log(` - ${f.replace(cwd + '/', '')}`))
53
- console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
54
- }
55
- }
56
-
57
- /**
58
- * 读取 design.md frontmatter 的 scale 字段(brainstorm 末步写入 'small'|'large')。
59
- * completeStep 的下一步提示据此分叉:small→quick,large/读不到→plan(fail-safe 走重流程)。
60
- * 只解析首个 YAML frontmatter 块,避免误读正文里的 "scale:"。
61
- */
62
- function readDesignScale(specBase, changeName) {
63
- if (!changeName) return null
64
- const designPath = join(specBase, 'changes', changeName, 'design.md')
65
- if (!existsSync(designPath)) return null
66
- const text = readFileSync(designPath, 'utf8')
67
- const fm = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
68
- if (!fm) return null
69
- const m = fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
70
- return m ? m[1] : null
71
- }
72
-
73
- /**
74
- * 验证关键文件是否存在于正确的变更目录下
75
- * 防止 AI 将文件写到错误的路径
76
- */
77
- function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
78
- const effectiveChange = changeName || progress.currentChange
79
- if (!effectiveChange) return
80
-
81
- const changeDir = join(specBase, 'changes', effectiveChange)
82
- if (!existsSync(changeDir)) return
83
-
84
- // 每个阶段完成后预期存在的文件
85
- const expectedFiles = {
86
- brainstorm: ['design.md', 'proposal.md', 'requirements.md', 'tasks.md'],
87
- plan: ['plan.md'],
88
- archive: ['module-impact.md'],
89
- }
90
-
91
- const expected = expectedFiles[stageName]
92
- if (!expected) return
93
-
94
- const missing = []
95
- for (const file of expected) {
96
- if (!existsSync(join(changeDir, file))) {
97
- missing.push(file)
98
- }
99
- }
100
-
101
- if (missing.length > 0) {
102
- console.log(`\n⚠️ 文件位置验证:以下文件未在变更目录中找到`)
103
- console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
104
- for (const f of missing) {
105
- // 检查是否写到了错误的位置
106
- const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
107
- if (existsSync(wrongPath)) {
108
- console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
109
- console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
110
- } else {
111
- console.log(` ⬜ ${f} — 未找到(该阶段可能未产出此文件)`)
112
- }
113
- }
114
- } else {
115
- console.log(`\n✅ 文件位置验证:所有 ${expected.length} 个预期文件均在变更目录中`)
116
- }
117
- }
28
+ // validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
29
+ // 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
30
+ // completeStep 阶段完成分支(task-02 接入 completeStageGates)+ brainstorm 下一步提示改 import 自 gates.js。
118
31
 
119
32
  /**
120
33
  * 坑1:用 --done --answer 解掉「已 waiting 的步骤」。
@@ -147,10 +60,9 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
147
60
  }
148
61
 
149
62
  export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
150
- const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, confirmMode = null, isForceBaseline = false, isAllowNew = false } = options
63
+ const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false } = options
151
64
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
152
65
  const stageData = progress.stages[stageName]
153
- const scanProfile = stageData?.scanProfile || null
154
66
 
155
67
  // ── WAIT MARKER 硬校验 ──
156
68
  // 如果 output 包含等待标记,拒绝 --done 推进
@@ -163,12 +75,6 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
163
75
  process.exit(1)
164
76
  }
165
77
  }
166
-
167
- // scanProfile 非 deep 模式:截断 outputText 减少 token 传递
168
- let effectiveOutput = outputText
169
- if (scanProfile && scanProfile.mode !== 'deep' && outputText && outputText.length > 1000) {
170
- effectiveOutput = outputText.slice(0, 1000) + '\n\n…[输出已截断,完整内容见 artifact]'
171
- }
172
78
  if (!stageData || !stageData.steps) {
173
79
  console.error(`❌ 阶段 ${stageName} 未初始化`)
174
80
  process.exit(1)
@@ -181,13 +87,29 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
181
87
  // requiresWait 步骤时会跳过它、把 --answer 静默丢弃,步骤永久卡 WAITING、末步报「等待用户输入」。
182
88
  // 修复:带 doneAnswer 且存在 waiting 步骤时,把首个 waiting 拉回 pending + 补 waitAnswer,主流程
183
89
  // requiresWait 门控见 waitAnswer 已置→不阻断→正常 completed。仅 --answer 触发,普通 --done 零变化。
184
- const _resolvedWaitIdx = resolveWaitingStepWithAnswer(steps, options && options.doneAnswer, new Date().toLocaleString('zh-CN', { hour12: false }))
90
+ const _doneAnswer = options && options.doneAnswer
91
+ // ── 多 waiting 歧义守卫(对齐 continueStep 742-751 的同名保护)──
92
+ // resolveWaitingStepWithAnswer 只 findIndex 解「第一个」waiting;阶段内 ≥2 个 waiting 时
93
+ // --done --answer 会静默把 answer 填给第一个,可能答错对象。与 --continue 对齐:多 waiting 时报错列出。
94
+ if (_doneAnswer) {
95
+ const waitingIdxs = steps.map((s, i) => (s && s.status === 'waiting') ? i : -1).filter(i => i !== -1)
96
+ if (waitingIdxs.length > 1) {
97
+ console.error(`❌ 检测到 ${waitingIdxs.length} 个等待中的步骤,--done --answer 无法确定解哪一个:`)
98
+ for (const i of waitingIdxs) {
99
+ console.error(` Step ${i + 1}: ${steps[i].name}${steps[i].waitReason ? `(${steps[i].waitReason})` : ''}`)
100
+ }
101
+ console.error(` 出路:用 --continue 逐个指定恢复(每次解一个 waiting,降到 1 个后即可 --done --answer):`)
102
+ console.error(` sillyspec run ${stageName} --continue --from-step <序号|名称> --answer "..."${changeName ? ` --change ${changeName}` : ''}`)
103
+ process.exit(1)
104
+ }
105
+ }
106
+ const _resolvedWaitIdx = resolveWaitingStepWithAnswer(steps, _doneAnswer, new Date().toLocaleString('zh-CN', { hour12: false }))
185
107
  if (_resolvedWaitIdx !== -1) {
186
108
  currentIdx = _resolvedWaitIdx
187
109
  console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
188
110
  }
189
111
  if (currentIdx === -1) {
190
- console.error('没有待完成的步骤')
112
+ console.error(`没有待完成的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
191
113
  process.exit(1)
192
114
  }
193
115
 
@@ -257,7 +179,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
257
179
  // archive「确认归档」收尾:--confirm 门控 + archiveChangeDirectory 移动 + 推荐文档校验
258
180
  // (抽成 handleArchiveConfirmStep;缺 --confirm 时返回 early-return 对象,completeStep 透传)
259
181
  {
260
- const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase })
182
+ const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts })
261
183
  if (_archiveEarlyReturn) return _archiveEarlyReturn
262
184
  }
263
185
 
@@ -311,14 +233,14 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
311
233
  if (hasWaiting) {
312
234
  // 有等待中的步骤,阶段未完成
313
235
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
314
- await pm._write(cwd, progress, changeName)
236
+ pm._write(cwd, progress, changeName)
315
237
  const wsIdx = steps.findIndex(s => s.status === 'waiting')
316
238
  console.log(`\n⏸️ 阶段暂停:Step ${wsIdx + 1} 等待用户输入`)
317
239
  if (steps[wsIdx].waitReason) console.log(` 原因:${steps[wsIdx].waitReason}`)
318
240
  return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
319
241
  }
320
242
  // quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion)
321
- await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts })
243
+ await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts, pm })
322
244
 
323
245
  // ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
324
246
  // nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
@@ -333,53 +255,37 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
333
255
  st.status = 'completed'
334
256
  st.completedAt = st.completedAt || nowStr
335
257
  }
336
- await pm._write(cwd, progress, changeName)
258
+ pm._write(cwd, progress, changeName)
337
259
  console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done,方案未变)`)
338
260
  }
339
261
 
340
262
  stageData.status = 'completed'
341
263
  stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
342
264
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
343
- await pm._write(cwd, progress, changeName)
344
- triggerSync(cwd, changeName, platformOpts)
265
+ // persist(_write + triggerSync)移到 completeStageGates 成功之后(task-01 / review-2026-08-09 #2):
266
+ // gate 任一段异常/失败 → rollbackCompletionAndReturn 回 in-progress 并落盘;此处未到 _write,DB 不留假 completed。
345
267
 
346
268
  // Append to user-inputs.md
347
269
  if (outputText) {
348
270
  const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
349
271
  const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
350
- appendFileSync(inputsPath, entry)
272
+ try {
273
+ appendFileSync(inputsPath, entry)
274
+ } catch (e) {
275
+ // best-effort:Windows AV/索引占用偶发 EPERM,历史日志写失败不阻断主流程(review-2026-08-09 #7,对齐 shared.js triggerSync)
276
+ console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
277
+ }
351
278
  }
352
279
 
353
- // scan 平台 manifest + post-check(W6 Step6d 抽至 complete-handlers.js handleScanStageCompleted)
354
- const _scanResult = await handleScanStageCompleted({ stageName, currentIdx, cwd, progress, pm, stageData, changeName, outputText, platformOpts })
355
- if (_scanResult) return _scanResult
356
-
357
- // 防御性守卫变量:确认所有步骤确实标记为 completed
358
- const actualCompleted = steps.filter(s => s.status === 'completed').length
359
- const actualTotal = steps.length
280
+ // 阶段完成收尾共享管线(handleScan manifest + validateMetadata/FileLocations + auxiliary 重置 +
281
+ // runStageCompletionGates + execute worktree cleanup),消除 S1/S2/S3 三处收尾不对称(task-01 抽出)。
282
+ // gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(合理收紧:回滚不该打完成提示)。
283
+ const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText })
284
+ if (_stageGatesResult) return _stageGatesResult
360
285
 
361
- validateMetadata(cwd, stageName, specBase)
362
-
363
- // 验证关键文件是否在正确的变更目录下(仅当所有步骤确实完成时才校验)
364
- if (actualCompleted === actualTotal && actualTotal > 0) {
365
- validateFileLocations(cwd, stageName, progress, changeName, specBase)
366
- }
367
-
368
- // 辅助阶段完成后重置步骤
369
- const stageDef = stageRegistry[stageName]
370
- if (stageDef?.auxiliary) {
371
- const freshSteps = (stageDef.steps || []).map(s => ({
372
- name: s.name,
373
- status: 'pending',
374
- output: null,
375
- completedAt: null
376
- }))
377
- stageData.steps = freshSteps
378
- stageData.status = 'pending'
379
- stageData.completedAt = null
380
- if (progress.currentStage === stageName) progress.currentStage = ''
381
- await pm._write(cwd, progress, changeName)
382
- }
286
+ // gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
287
+ pm._write(cwd, progress, changeName)
288
+ triggerSync(cwd, changeName, platformOpts)
383
289
 
384
290
  const total = steps.length
385
291
  console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
@@ -434,30 +340,22 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
434
340
  }
435
341
  }
436
342
 
437
- // 阶段完成校验 — 防御性守卫:仅当所有步骤确实标记为 completed 时才跑 validator
438
- if (actualCompleted === actualTotal && actualTotal > 0) {
439
- const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx })
440
- if (_gateEarlyReturn) return _gateEarlyReturn
441
- } else if (actualCompleted < actualTotal) {
442
- // 实际步骤未全部完成,跳过 validator(状态可能不同步)
443
- console.log(`\n⚠️ 阶段校验跳过:${actualTotal} 步中仅 ${actualCompleted} 步标记为已完成,可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
444
- }
445
-
446
- // execute worktree cleanup(W6 Step6c 抽至 complete-handlers.js handleExecuteWorktreeCleanup)
447
- await handleExecuteWorktreeCleanup({ stageName, changeName, cwd })
448
-
449
343
  return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
450
344
  }
451
345
 
452
346
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
453
- await pm._write(cwd, progress, changeName)
347
+ pm._write(cwd, progress, changeName)
454
348
  triggerSync(cwd, changeName, platformOpts)
455
349
 
456
350
  // Append to user-inputs.md
457
351
  if (outputText) {
458
352
  const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
459
353
  const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
460
- appendFileSync(inputsPath, entry)
354
+ try {
355
+ appendFileSync(inputsPath, entry)
356
+ } catch (e) {
357
+ console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
358
+ }
461
359
  }
462
360
 
463
361
  const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
@@ -469,6 +367,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
469
367
 
470
368
  if (printNext) {
471
369
  await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
370
+ // task-08:底部锚定行——outputStep 渲染的长 prompt 易被 tail 截断,末尾再打一行推进位置,
371
+ // 让 agent 只看末几行也能知道「推进到第几步」。仅单步推进分支(阶段完成分支已有 ✅ 阶段已完成)。
372
+ // defSteps 越界防御:平台模式 buildPlanSteps 长度漂移时 nextPendingIdx 可能越界
373
+ // (参照 prompt.js:174-179 越界降级先例),越界时静默跳过,不崩。
374
+ if (defSteps && defSteps[nextPendingIdx]) {
375
+ console.log(`\n🚀 advanced to step ${nextPendingIdx + 1}/${steps.length}: ${defSteps[nextPendingIdx].name}`)
376
+ }
472
377
  }
473
378
  return { stageCompleted: false, currentIdx, nextPendingIdx }
474
379
  }
@@ -482,6 +387,31 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
482
387
  * @returns {{autoChecked:boolean, checkedCount:number, skippedCount:number, planTotal:number, planChecked:number}}
483
388
  * planTotal/planChecked 在调用方于本函数之后用 readPlanCheckboxStatus 重读(勾选已落盘)。
484
389
  */
390
+ /**
391
+ * 读 task 卡片文本(plan.md task 行外的详细描述/allowed_paths),用于端到端 task 判定。
392
+ * 卡片缺失返回空串(仅靠 plan.md task 行文本判)。
393
+ */
394
+ function readTaskCardText(changeDir, taskNum) {
395
+ try { return readFileSync(join(changeDir, 'tasks', `task-${taskNum}.md`), 'utf8') } catch { return '' }
396
+ }
397
+
398
+ /**
399
+ * 判定 task 是否该被 autoCheckPlanFromReviews 自动勾选。
400
+ * 端到端/deployment-critical task 要求 review spec+quality 双 pass(cannot_verify 不算,防批量完成
401
+ * 放行未真验的端到端 task);普通 task 非 fail 即可(保主 agent 直接实现模式体验,其 cannot_verify
402
+ * 草稿仍可批量收尾)。坑 execute-batch-complete-endtoend-checkbox。
403
+ * @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string}}} r readReview 结果
404
+ * @param {boolean} endToEnd 是否端到端/deployment-critical task
405
+ * @returns {boolean}
406
+ */
407
+ export function shouldAutoCheckTask(r, endToEnd) {
408
+ if (!r?.ok) return false
409
+ const spec = r.review?.specVerdict
410
+ const quality = r.review?.qualityVerdict
411
+ if (spec === 'fail' || quality === 'fail') return false
412
+ if (endToEnd) return spec === 'pass' && quality === 'pass'
413
+ return true
414
+ }
485
415
  async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
486
416
  if (stageName !== 'execute' || !changeName) {
487
417
  return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
@@ -490,31 +420,40 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
490
420
  const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
491
421
  const changeDir = join(specBaseLc, 'changes', changeName)
492
422
  const planPath = join(changeDir, 'plan.md')
493
- const runtimeRoot = platformOpts?.runtimeRoot || join(specBaseLc, '.runtime')
423
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, specBaseLc)
494
424
  const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
495
425
  if (!existsSync(planPath) || !existsSync(runIdFile)) {
496
426
  return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
497
427
  }
498
428
  const executeRunId = readFileSync(runIdFile, 'utf8').trim()
499
- const planContent = readFileSync(planPath, 'utf8')
500
429
  const { readReview } = await import('../task-review.js')
501
- let checkedCount = 0
502
- let skippedCount = 0
503
- const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
504
- const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
505
- const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
506
- const r = readReview(reviewPath)
507
- if (r.ok && r.review?.specVerdict !== 'fail' && r.review?.qualityVerdict !== 'fail') {
508
- checkedCount++
509
- return `${p1}x${p2}` // 勾选
430
+ // plan.md 是 agent 与 CLI 都会写的共享文件(agent 勾 checkbox、此处 autoCheck 也勾选)。
431
+ // 读-改-写必须整体持锁(withFileLock 串行化多进程),否则并发 execute --done / 手动勾选互相覆盖
432
+ // (后到者覆盖先到者);写入用 writeAtomicSync(tmp+rename 原子),防 Windows 整文件覆盖被读半截
433
+ // (fs-atomic.js 头注明的 reader-writer 竞态坑)。锁文件放 changeDir,与 QUICKLOG 同机制。
434
+ const lockPath = join(changeDir, '.plan.md.lock')
435
+ return await withFileLock(lockPath, async () => {
436
+ const planContent = readFileSync(planPath, 'utf8')
437
+ let checkedCount = 0
438
+ let skippedCount = 0
439
+ const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
440
+ const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
441
+ const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
442
+ const r = readReview(reviewPath)
443
+ // 端到端 task:必须 pass(cannot_verify 不算);普通 task:非 fail 即可(坑 execute-batch-complete-endtoend-checkbox)
444
+ const endToEnd = isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
445
+ if (shouldAutoCheckTask(r, endToEnd)) {
446
+ checkedCount++
447
+ return `${p1}x${p2}` // 勾选
448
+ }
449
+ skippedCount++
450
+ return match // 不勾
451
+ })
452
+ if (checkedCount > 0) {
453
+ writeAtomicSync(planPath, updated)
510
454
  }
511
- skippedCount++
512
- return match // 不勾
455
+ return { autoChecked: checkedCount > 0, checkedCount, skippedCount }
513
456
  })
514
- if (checkedCount > 0) {
515
- writeFileSync(planPath, updated)
516
- }
517
- return { autoChecked: checkedCount > 0, checkedCount, skippedCount }
518
457
  } catch {
519
458
  return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
520
459
  }
@@ -592,7 +531,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
592
531
  // 查找下一个 pending 或 in-progress 的步骤
593
532
  const currentIdx = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
594
533
  if (currentIdx === -1) {
595
- console.error('没有可以等待的步骤')
534
+ console.error(`没有可以等待的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
596
535
  process.exit(1)
597
536
  }
598
537
 
@@ -651,7 +590,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
651
590
  }
652
591
 
653
592
  progress.lastActive = now
654
- await pm._write(cwd, progress, changeName)
593
+ pm._write(cwd, progress, changeName)
655
594
  triggerSync(cwd, changeName, platformOpts)
656
595
 
657
596
  console.log(`⏸️ Step ${currentIdx + 1}/${stageData.steps.length} 已暂停等待:${stageData.steps[currentIdx].name}`)
@@ -678,7 +617,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
678
617
  // 查找 waiting 的步骤
679
618
  const waitingSteps = stageData.steps.map((s, i) => ({ ...s, idx: i })).filter(s => s.status === 'waiting')
680
619
  if (waitingSteps.length === 0) {
681
- console.error('没有处于等待状态的步骤')
620
+ console.error(`没有处于等待状态的步骤(阶段 ${stageName} 当前无 waiting 步骤,--continue 无目标)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
682
621
  process.exit(1)
683
622
  }
684
623
 
@@ -754,7 +693,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
754
693
  }
755
694
 
756
695
  progress.lastActive = now
757
- await pm._write(cwd, progress, changeName)
696
+ pm._write(cwd, progress, changeName)
758
697
  triggerSync(cwd, changeName, platformOpts)
759
698
 
760
699
  console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${currentStep.name}`)
@@ -763,7 +702,11 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
763
702
  // Append to user-inputs.md
764
703
  const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
765
704
  const entry = `\n## ${now} | ${changeName || '?'} | ${stageName}: ${currentStep.name} [CONTINUED]\n- 回答:${answer}\n`
766
- appendFileSync(inputsPath, entry)
705
+ try {
706
+ appendFileSync(inputsPath, entry)
707
+ } catch (e) {
708
+ console.warn('⚠️ user-inputs.md 追加失败(不阻断):', e.message)
709
+ }
767
710
 
768
711
  // shouldReturnToCurrentStep: 回到当前步骤继续执行(repeatable=多轮探索,requiresWait=确认后执行动作)
769
712
  if (shouldReturnToCurrentStep) {
@@ -793,37 +736,14 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
793
736
  if (nextPendingIdx === -1 && nextWaitingIdx === -1) {
794
737
  stageData.status = 'completed'
795
738
  stageData.completedAt = now
796
- await pm._write(cwd, progress, changeName)
739
+ // persist _write 移到 completeStageGates 成功之后(task-03 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
740
+ // 阶段完成收尾共享管线(含 execute worktree cleanup),消除 continueStep 完成分支绕过 gate 的 S2(task-01 抽出)。
741
+ // gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(与 completeStep 同语义)。
742
+ const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null })
743
+ if (_stageGatesResult) return _stageGatesResult
744
+ // gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
745
+ pm._write(cwd, progress, changeName)
797
746
  console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
798
- // ── execute 阶段完成时条件性清理 worktree ──
799
- if (stageName === 'execute' && changeName) {
800
- try {
801
- const { WorktreeManager } = await import('../worktree.js');
802
- const wm = new WorktreeManager({ cwd });
803
- const meta = wm.getMeta(changeName);
804
- if (!meta) {
805
- console.log('🔗 Worktree: n/a (no meta)');
806
- } else if (meta.mode === 'native-worktree') {
807
- console.log('🔗 Worktree: kept (外部隔离环境)');
808
- } else {
809
- // in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)
810
- const check = wm.hasUnappliedChanges(changeName);
811
- if (check.hasChanges) {
812
- console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
813
- console.log(` 下一步: sillyspec worktree apply ${changeName}`);
814
- } else {
815
- const cleanResult = wm.cleanup(changeName);
816
- console.log(`🔗 Worktree: ${cleanResult.result}`);
817
- if (cleanResult.residual?.length > 0) {
818
- console.warn(` ⚠️ 清理残留: ${cleanResult.residual.join('; ')}`);
819
- console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
820
- }
821
- }
822
- }
823
- } catch (e) {
824
- console.warn(`🔗 Worktree: check failed — ${e.message}`);
825
- }
826
- }
827
747
  // 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
828
748
  const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
829
749
  if (nextStageHint) {
@@ -884,7 +804,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
884
804
  console.error(`⏸️ Step ${wsIdx + 1} 正在等待用户输入,不能跳过。`)
885
805
  console.error(` 请先使用 --continue --answer "..." 继续,或用 --reset 重置。`)
886
806
  } else {
887
- console.error('没有待跳过的步骤')
807
+ console.error(`没有待跳过的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
888
808
  }
889
809
  process.exit(1)
890
810
  }
@@ -899,7 +819,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
899
819
  steps[currentIdx].status = 'skipped'
900
820
  steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
901
821
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
902
- await pm._write(cwd, progress, changeName)
822
+ pm._write(cwd, progress, changeName)
903
823
  triggerSync(cwd, changeName, platformOpts)
904
824
 
905
825
  console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)