sillyspec 3.26.10 → 3.26.11

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 (97) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +85 -85
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
  4. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  5. package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
  6. package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
  7. package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
  8. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  9. package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
  10. package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
  11. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  12. package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
  13. package/.claude/skills/sillyspec-state/SKILL.md +64 -64
  14. package/.claude/skills/sillyspec-status/SKILL.md +54 -54
  15. package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
  16. package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
  17. package/.gitattributes +2 -0
  18. package/.husky/pre-push +20 -20
  19. package/.npmrc-publish +2 -0
  20. package/README.md +201 -201
  21. package/SKILL.md +90 -90
  22. package/package.json +39 -39
  23. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  24. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  25. package/packages/dashboard/dist/index.html +16 -16
  26. package/packages/dashboard/package.json +25 -25
  27. package/packages/dashboard/server/executor.js +86 -86
  28. package/packages/dashboard/server/index.js +601 -601
  29. package/packages/dashboard/server/parser.js +526 -526
  30. package/src/change-risk-profile.js +254 -254
  31. package/src/classify-change.js +109 -109
  32. package/src/config-schema.js +310 -310
  33. package/src/contract-matrix.js +379 -379
  34. package/src/db.js +350 -350
  35. package/src/dispatch/backends/local-agent.js +155 -155
  36. package/src/dispatch/backends/sillyhub-mcp.js +245 -245
  37. package/src/dispatch/probe.js +210 -210
  38. package/src/dispatch/strategy.js +121 -121
  39. package/src/docs-check.js +131 -4
  40. package/src/doctor-diagnostics.js +711 -711
  41. package/src/endpoint-extractor.js +401 -401
  42. package/src/fs-atomic.js +69 -69
  43. package/src/git-helper.js +79 -79
  44. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  45. package/src/hooks/worktree-guard.js +848 -848
  46. package/src/index.js +90 -8
  47. package/src/init.js +623 -623
  48. package/src/knowledge-match.js +130 -130
  49. package/src/local-detect.js +120 -120
  50. package/src/machine-interface.js +543 -543
  51. package/src/migrate.js +127 -127
  52. package/src/progress/change-registry.js +297 -260
  53. package/src/progress/consistency-doctor.js +355 -355
  54. package/src/progress/shared.js +36 -36
  55. package/src/progress/stage-machine.js +29 -1
  56. package/src/progress/step-store.js +261 -261
  57. package/src/progress.js +1131 -1127
  58. package/src/quicklog.js +693 -693
  59. package/src/review-tier.js +77 -77
  60. package/src/run/command.js +1385 -1364
  61. package/src/run/complete-handlers.js +1443 -1399
  62. package/src/run/complete.js +225 -24
  63. package/src/run/gates.js +755 -755
  64. package/src/run/prompt.js +761 -761
  65. package/src/run/quick-audit.js +155 -146
  66. package/src/run/shared.js +1268 -1215
  67. package/src/run/stage.js +487 -479
  68. package/src/scan-postcheck.js +371 -371
  69. package/src/setup.js +416 -416
  70. package/src/sillyhub-mcp/client.js +529 -529
  71. package/src/spec-sync.js +323 -319
  72. package/src/stage-contract.js +897 -897
  73. package/src/stage-review.js +549 -549
  74. package/src/stages/archive.js +155 -155
  75. package/src/stages/brainstorm-auto.js +236 -236
  76. package/src/stages/brainstorm.js +596 -597
  77. package/src/stages/cmd-existence.js +145 -145
  78. package/src/stages/doctor.js +371 -371
  79. package/src/stages/execute.js +1003 -1003
  80. package/src/stages/explore.js +34 -34
  81. package/src/stages/index.js +29 -29
  82. package/src/stages/knowledge.js +498 -498
  83. package/src/stages/plan-postcheck.js +1302 -1302
  84. package/src/stages/plan.js +625 -623
  85. package/src/stages/quick.js +151 -151
  86. package/src/stages/scan.js +607 -607
  87. package/src/stages/status.js +64 -64
  88. package/src/stages/verify.js +264 -264
  89. package/src/sync.js +1223 -1135
  90. package/src/task-review.js +1019 -1019
  91. package/src/verify-postcheck.js +1186 -1186
  92. package/src/workflow.js +600 -600
  93. package/src/worktree-apply.js +1213 -1136
  94. package/src/worktree-deps.js +353 -353
  95. package/src/worktree.js +1481 -1481
  96. package/templates/workflows/archive-impact.yaml +79 -79
  97. package/templates/workflows/scan-docs.yaml +132 -132
@@ -29,11 +29,25 @@ import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProject
29
29
  import { formatExecuteSummary } from '../worktree-apply.js'
30
30
  import { isEndToEndTaskText } from '../change-risk-profile.js'
31
31
  import { deriveTitleFromLinkedChange } from '../quicklog.js'
32
+ import { gitQuiet } from '../git-helper.js'
32
33
 
33
34
  // validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
34
35
  // 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
35
36
  // completeStep 阶段完成分支(task-02 接入 completeStageGates)+ brainstorm 下一步提示改 import 自 gates.js。
36
37
 
38
+ // 完整流程 change title 刷新:从 proposal/design 首个 # 标题提取中文描述写 changes.title(人类可读
39
+ // 展示元信息)。proposal/design 落盘后的**任意步骤持久化点**都调用——单步 --done(brainstorm
40
+ // step6 design.md 已落盘但阶段未完)与阶段完成同样刷新,治「brainstorm 全程 title 存英文 autoName
41
+ // 兜底」。quick-<hex> 无 proposal/design 目录,deriveTitleFromLinkedChange 返回 '' 不刷新
42
+ // (quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。失败静默(不阻断流程)。
43
+ function refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName) {
44
+ if (!changeName || /^quick-[0-9a-f]{8}$/.test(changeName)) return
45
+ try {
46
+ const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
47
+ if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
48
+ } catch { /* title 刷新失败不阻断 */ }
49
+ }
50
+
37
51
  /**
38
52
  * 坑1:用 --done --answer 解掉「已 waiting 的步骤」。
39
53
  *
@@ -114,8 +128,28 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
114
128
  console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
115
129
  }
116
130
  if (currentIdx === -1) {
117
- console.error(`没有待完成的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
118
- process.exit(1)
131
+ // ── reopen stale 收尾路径(坑 reopen-done-escape-hatch-unreachable)──
132
+ // 全 completed+stale(无 pending/in-progress)时若只有 exit(1),--done --confirm 逃生门
133
+ // 永远到不了下方回填门控。修复:有 stale 时——无 confirm 报拦截指引(与门控同文案);
134
+ // 带 confirm 把首个 stale 拉回 pending 走完整完成管线,末步后的回填门控 confirm 分支
135
+ // 收尾其余 stale(--confirm 语义 = 全部 stale 一并确认)。
136
+ const _staleIdx = steps.findIndex(s => s.status === 'stale')
137
+ if (_staleIdx !== -1) {
138
+ const _staleNames = steps.filter(s => s.status === 'stale').map(s => s.name)
139
+ if (!confirm) {
140
+ console.error(`\n⏸️ 检测到 ${_staleNames.length} 个 stale 步骤(reopen 后未执行):${_staleNames.join('、')}`)
141
+ console.error(` 两条出路:`)
142
+ console.error(` ① sillyspec run ${stageName}${changeName ? ` --change ${changeName}` : ''} 逐个真实执行(stale 会被转为 pending)`)
143
+ console.error(` ② 确认方案未变,一次性回填收尾:sillyspec run ${stageName} --done --confirm${changeName ? ` --change ${changeName}` : ''} --output "..."`)
144
+ process.exit(1)
145
+ }
146
+ steps[_staleIdx].status = 'pending'
147
+ currentIdx = _staleIdx
148
+ console.log(`⚠️ --confirm 收尾:把 stale 步骤 "${steps[_staleIdx].name}" 拉回当前步骤走完成管线,其余 stale 由回填门控一并确认。`)
149
+ } else {
150
+ console.error(`没有待完成的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
151
+ process.exit(1)
152
+ }
119
153
  }
120
154
 
121
155
  // ── requiresWait 硬门控 ──
@@ -285,15 +319,40 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
285
319
  // completed,否则进度数字与实际步骤明细矛盾(阶段已完成 + 6/8)。
286
320
  // 安全边界:仅进入阶段完成分支时才回填(stale 本就被跳过、不回填也照常标 completed),
287
321
  // 不改变单步推进行为;stale 若确需重跑,运行流(stage.js:141-148)会先于本分支转 pending 执行。
322
+ // FR-01 门控:无 --confirm 时不回填,阻断并给两条出路;带 --confirm 时回填并审计。
288
323
  const staleSteps = steps.filter(s => s.status === 'stale')
289
324
  if (staleSteps.length > 0) {
325
+ if (!confirm) {
326
+ // 无 confirm:不回填,阶段不完成,返回 staleBlocked 标记
327
+ const staleNames = staleSteps.map(s => `Step ${steps.indexOf(s) + 1}(${s.name})`).join(', ')
328
+ console.error(`⏸️ 检测到 ${staleSteps.length} 个 stale 步骤(reopen 后未执行)——两条出路:`)
329
+ console.error(` ① sillyspec run ${stageName} 逐个真实执行(首个 stale 自动转 pending)`)
330
+ console.error(` ② 确认方案未变用 --done --confirm 一次性回填收尾`)
331
+ console.error(` stale 步骤:${staleNames}`)
332
+ progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
333
+ pm._write(cwd, progress, changeName)
334
+ return { stageCompleted: false, currentIdx, nextPendingIdx: -1, staleBlocked: true }
335
+ }
336
+ // 带 confirm:按现行回填逻辑 + 审计记录
290
337
  const nowStr = new Date().toLocaleString('zh-CN',{hour12:false})
291
338
  for (const st of staleSteps) {
292
339
  st.status = 'completed'
293
340
  st.completedAt = st.completedAt || nowStr
294
341
  }
295
342
  pm._write(cwd, progress, changeName)
296
- console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done,方案未变)`)
343
+ console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done --confirm,方案未变)`)
344
+ // 审计记录 reopen-stale-backfill
345
+ try {
346
+ pm._appendAuditLog(cwd, {
347
+ action: 'reopen-stale-backfill',
348
+ stage: stageName,
349
+ change: changeName,
350
+ steps: staleSteps.map(s => s.name),
351
+ confirmedAt: nowStr
352
+ })
353
+ } catch (e) {
354
+ console.warn('⚠️ 回填审计日志写入失败(不阻断):', e.message)
355
+ }
297
356
  }
298
357
 
299
358
  stageData.status = 'completed'
@@ -331,15 +390,8 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
331
390
  if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
332
391
  if (_stageGatesResult) return _stageGatesResult
333
392
 
334
- // 完整流程 change title 刷新:proposal/design 落盘后(brainstorm/plan 完成),从 proposal 首个 #
335
- // 标题刷新 changes.title(人类可读中文标题,用户可随时刷新)。quick-<hex> 无 proposal 目录 →
336
- // deriveTitleFromLinkedChange 返回 '' 不刷新(quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。
337
- if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
338
- try {
339
- const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
340
- if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
341
- } catch { /* title 刷新失败不阻断完成 */ }
342
- }
393
+ // 完整流程 change title 刷新(阶段完成时;proposal/design 已落盘,机制见 refreshChangeTitleFromArtifacts)
394
+ refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
343
395
 
344
396
  // gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
345
397
  pm._write(cwd, progress, changeName)
@@ -404,6 +456,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
404
456
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
405
457
  pm._write(cwd, progress, changeName)
406
458
  triggerSync(cwd, changeName, platformOpts)
459
+ // 单步完成也刷新 change title:design.md 在 brainstorm step6 落盘,此后每次 --done 都该让
460
+ // changes.title 反映中文描述,不等阶段收尾(机制见 refreshChangeTitleFromArtifacts)。
461
+ refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
407
462
 
408
463
  // Append to user-inputs.md
409
464
  if (outputText) {
@@ -453,21 +508,90 @@ function readTaskCardText(changeDir, taskNum) {
453
508
  try { return readFileSync(join(changeDir, 'tasks', `task-${taskNum}.md`), 'utf8') } catch { return '' }
454
509
  }
455
510
 
511
+ /**
512
+ * W2 task-04/05 共用 helper:构造草稿零 diff 校验用的 ctx(与 task-review.js generateTaskReviewDrafts 同源)。
513
+ * 用于 autoCheckPlanFromReviews(勾选层)和 detectExecuteBatchFinish(批量层)。
514
+ * 失败返回 null(调用方降级:勾选层不勾、批量层不过滤——向后兼容)。
515
+ *
516
+ * @returns {Promise<{gitDir:string, base:string, head:string}|null>}
517
+ */
518
+ async function buildDraftContext(cwd, changeName) {
519
+ try {
520
+ const { WorktreeManager } = await import('../worktree.js')
521
+ const meta = new WorktreeManager({ cwd }).getMeta(changeName)
522
+ const base = meta?.baselineCommit || meta?.baseHash || null
523
+ let gitDir = cwd
524
+ let head = null
525
+ if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
526
+ gitDir = meta.worktreePath
527
+ }
528
+ if (base) {
529
+ try {
530
+ head = gitQuiet(gitDir, ['rev-parse', 'HEAD'])
531
+ } catch (e) {
532
+ console.warn(`⚠️ ctx 构造失败(git rev-parse HEAD 失败:${e && e.message ? e.message : e}),跳过草稿 diff 校验`)
533
+ return null
534
+ }
535
+ }
536
+ if (base && head && gitDir) {
537
+ return { gitDir, base, head }
538
+ }
539
+ return null
540
+ } catch (e) {
541
+ console.warn(`⚠️ ctx 构造失败(${e && e.message ? e.message : e}),跳过草稿 diff 校验`)
542
+ return null
543
+ }
544
+ }
545
+
456
546
  /**
457
547
  * 判定 task 是否该被 autoCheckPlanFromReviews 自动勾选。
458
548
  * 端到端/deployment-critical task 要求 review spec+quality 双 pass(cannot_verify 不算,防批量完成
459
549
  * 放行未真验的端到端 task);普通 task 非 fail 即可(保主 agent 直接实现模式体验,其 cannot_verify
460
550
  * 草稿仍可批量收尾)。坑 execute-batch-complete-endtoend-checkbox。
461
- * @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string}}} r readReview 结果
551
+ *
552
+ * W2 task-04 FR-03:草稿勾选层零 diff 守卫——review 为自动草稿且 ctx 给定时,额外要求
553
+ * changedFiles 非空且 git diff 实测非空。防 allowed_paths 误归属他人 diff 或陈旧 review.json 导致
554
+ * 空任务被自动勾选/批量放行。ctx 缺省时保持现行判定(向后兼容)。
555
+ *
556
+ * @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string, reviewerNotes?:string, changedFiles?:string[]}}} r readReview 结果
462
557
  * @param {boolean} endToEnd 是否端到端/deployment-critical task
558
+ * @param {{gitDir?:string, base?:string, head?:string}|null} [ctx] 实测 diff 上下文;缺省保持现行判定
463
559
  * @returns {boolean}
464
560
  */
465
- export function shouldAutoCheckTask(r, endToEnd) {
561
+ export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
466
562
  if (!r?.ok) return false
467
563
  const spec = r.review?.specVerdict
468
564
  const quality = r.review?.qualityVerdict
469
565
  if (spec === 'fail' || quality === 'fail') return false
470
566
  if (endToEnd) return spec === 'pass' && quality === 'pass'
567
+
568
+ // W2 task-04 草稿零 diff 守卫(FR-03)
569
+ // ctx 给定且 review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空
570
+ if (ctx && r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
571
+ const changedFiles = r.review.changedFiles || []
572
+ if (changedFiles.length === 0) {
573
+ console.warn(`⚠️ 草稿 review changedFiles 为空,跳过自动勾选`)
574
+ return false
575
+ }
576
+ if (!ctx.gitDir || !ctx.base || !ctx.head) {
577
+ console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff 校验,保守不勾`)
578
+ return false
579
+ }
580
+ try {
581
+ // 实测 diff:git diff --name-only <base>..<head> -- <changedFiles>
582
+ // 用数组参数避免 shell 拆词,复用 git-helper 安全模式
583
+ const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
584
+ if (!diffResult || diffResult.trim() === '') {
585
+ console.warn(`⚠️ 草稿 review 实测 diff 为空(base=${ctx.base.slice(0,8)}, head=${ctx.head.slice(0,8)}, files=${changedFiles.length}),跳过自动勾选`)
586
+ return false
587
+ }
588
+ console.log(` ✓ 草稿 diff 校验通过(${diffResult.trim().split('\n').length} 个文件有改动)`)
589
+ } catch (e) {
590
+ console.warn(`⚠️ 草稿 diff 校验失败(${e && e.message ? e.message : e}),保守不勾`)
591
+ return false
592
+ }
593
+ }
594
+
471
595
  return true
472
596
  }
473
597
  async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
@@ -491,6 +615,10 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
491
615
  return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
492
616
  }
493
617
  const executeRunId = c
618
+
619
+ // W2 task-04 FR-03:构造 ctx 供草稿零 diff 守卫用(与 task-review.js generateTaskReviewDrafts 同源)
620
+ const ctx = await buildDraftContext(cwd, changeName)
621
+
494
622
  // plan.md 是 agent 与 CLI 都会写的共享文件(agent 勾 checkbox、此处 autoCheck 也勾选)。
495
623
  // 读-改-写必须整体持锁(withFileLock 串行化多进程),否则并发 execute --done / 手动勾选互相覆盖
496
624
  // (后到者覆盖先到者);写入用 writeAtomicSync(tmp+rename 原子),防 Windows 整文件覆盖被读半截
@@ -506,7 +634,7 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
506
634
  const r = readReview(reviewPath)
507
635
  // 端到端 task:必须 pass(cannot_verify 不算);普通 task:非 fail 即可(坑 execute-batch-complete-endtoend-checkbox)
508
636
  const endToEnd = isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
509
- if (shouldAutoCheckTask(r, endToEnd)) {
637
+ if (shouldAutoCheckTask(r, endToEnd, ctx)) {
510
638
  checkedCount++
511
639
  return `${p1}x${p2}` // 勾选
512
640
  }
@@ -530,8 +658,11 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
530
658
  * 复用现成能力:readPlanCheckboxStatus(plan 勾选状态)+ checkExecuteCodeEvidence(代码客观核验,
531
659
  * 与 doctor --align-execute-progress 同源,D-002/D-004)。仅在 stageName==='execute' && changeName 触发。
532
660
  *
661
+ * W2 task-05 FR-04:plan 全勾后逐 task 复核——review 缺失或草稿零 diff → 阻断批量,
662
+ * 返回 blockedTasks 列表(task id 数组),调用方打印 reason。
663
+ *
533
664
  * 安全门:plan 零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任 plan 声明但用代码核验兜底。
534
- * @returns {Promise<{batched:boolean, aligned:number, reason?:string}>}
665
+ * @returns {Promise<{batched:boolean, aligned:number, reason?:string, blockedTasks?:string[]}>}
535
666
  */
536
667
  async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps }) {
537
668
  if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
@@ -548,6 +679,78 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
548
679
  if (evidence.status === 'unchanged') {
549
680
  return { batched: false, aligned: 0, reason: `代码零变更(${evidence.detail})` }
550
681
  }
682
+
683
+ // W2 task-05 FR-04:逐 task 复核(草稿零 diff 守卫——批量层)
684
+ // 读取 plan.md 提取已勾 task id 列表(正则 `- [x] task-NN`)
685
+ const planPath = join(changeDir, 'plan.md')
686
+ const planContent = readFileSync(planPath, 'utf8')
687
+ const taskCheckboxRegex = /-\s\[x\]\s+task-(\d+)/gi
688
+ const taskMatches = [...planContent.matchAll(taskCheckboxRegex)]
689
+ const taskIds = taskMatches.map(m => `task-${m[1].padStart(2, '0')}`)
690
+
691
+ if (taskIds.length === 0) {
692
+ return { batched: false, aligned: 0, reason: 'plan.md 无已勾 task checkbox' }
693
+ }
694
+
695
+ // 构造 ctx 供草稿零 diff 校验用(与 task-04 同源)
696
+ const ctx = await buildDraftContext(cwd, changeName)
697
+ const runtimeRoot = resolveRuntimeRoot({ specRoot: specBase }, specBase)
698
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
699
+ const blockedTasks = []
700
+
701
+ for (const taskId of taskIds) {
702
+ let review = null
703
+ try {
704
+ const { readReview, isValidExecuteRunId } = await import('../task-review.js')
705
+ const c = readFileSync(runIdFile, 'utf8').trim()
706
+ if (!c || !isValidExecuteRunId(c)) {
707
+ blockedTasks.push(taskId)
708
+ continue
709
+ }
710
+ const executeRunId = c
711
+ const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
712
+ const r = readReview(reviewPath)
713
+ if (!r?.ok) {
714
+ blockedTasks.push(taskId)
715
+ continue
716
+ }
717
+ review = r.review
718
+ } catch {
719
+ blockedTasks.push(taskId)
720
+ continue
721
+ }
722
+
723
+ // review 存在:判定是否为自动草稿且零 diff
724
+ if (review?.reviewerNotes && review.reviewerNotes.includes('auto-generated draft')) {
725
+ const changedFiles = review.changedFiles || []
726
+ // 草稿且 changedFiles 为空 → 阻断
727
+ if (changedFiles.length === 0) {
728
+ blockedTasks.push(taskId)
729
+ continue
730
+ }
731
+ // 草稿且有 changedFiles → 实测 diff 校验(ctx 与 task-04 同源)
732
+ if (ctx && ctx.gitDir && ctx.base && ctx.head) {
733
+ try {
734
+ const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
735
+ if (!diffResult || diffResult.trim() === '') {
736
+ blockedTasks.push(taskId)
737
+ }
738
+ } catch (e) {
739
+ // diff 校验失败保守处理:阻断(避免误批量)
740
+ blockedTasks.push(taskId)
741
+ }
742
+ }
743
+ }
744
+ }
745
+
746
+ // blockedTasks 非空 → 阻断批量
747
+ if (blockedTasks.length > 0) {
748
+ const reason = `以下 task 复核未过(review 缺失或草稿零 diff):${blockedTasks.join(', ')}`
749
+ console.warn(`⚠️ 批量完成被阻断:${reason}`)
750
+ return { batched: false, aligned: 0, reason, blockedTasks }
751
+ }
752
+
753
+ // 全部复核通过 → 批量标 completed
551
754
  const now = new Date().toLocaleString('zh-CN', { hour12: false })
552
755
  let aligned = 0
553
756
  for (const step of steps) {
@@ -558,8 +761,9 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
558
761
  }
559
762
  }
560
763
  return { batched: true, aligned }
561
- } catch {
562
- return { batched: false, aligned: 0 }
764
+ } catch (e) {
765
+ // 异常 fail-open:不批量,reason 带错误信息
766
+ return { batched: false, aligned: 0, reason: `批量完成检测异常:${e && e.message ? e.message : e}` }
563
767
  }
564
768
  }
565
769
 
@@ -759,6 +963,8 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
759
963
  progress.lastActive = now
760
964
  pm._write(cwd, progress, changeName)
761
965
  triggerSync(cwd, changeName, platformOpts)
966
+ // wait 解除持久化点(含 repeatable 多轮)同样刷新 change title——多轮确认期间 design.md 已存在
967
+ refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
762
968
 
763
969
  console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${currentStep.name}`)
764
970
  console.log(` 回答:${answer}`)
@@ -815,12 +1021,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
815
1021
  if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
816
1022
  if (_stageGatesResult) return _stageGatesResult
817
1023
  // 完整流程 change title 刷新(同 completeStep 完成分支,wait 解除后阶段完成也刷新)。
818
- if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
819
- try {
820
- const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
821
- if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
822
- } catch { /* title 刷新失败不阻断完成 */ }
823
- }
1024
+ refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
824
1025
  // gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
825
1026
  pm._write(cwd, progress, changeName)
826
1027
  console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)