sillyspec 3.25.4 → 3.25.6

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 (41) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +27 -1
  2. package/.claude/skills/sillyspec-execute/SKILL.md +56 -0
  3. package/.claude/skills/sillyspec-plan/SKILL.md +26 -0
  4. package/.claude/skills/sillyspec-propose/SKILL.md +26 -0
  5. package/.claude/skills/sillyspec-scan/SKILL.md +12 -7
  6. package/.claude/skills/sillyspec-verify/SKILL.md +2 -2
  7. package/package.json +1 -1
  8. package/src/change-list.js +114 -12
  9. package/src/change-risk-profile.js +36 -1
  10. package/src/constants.js +11 -0
  11. package/src/index.js +4 -1
  12. package/src/machine-interface.js +5 -1
  13. package/src/progress/stage-machine.js +17 -14
  14. package/src/run/command.js +58 -3
  15. package/src/run/complete-handlers.js +9 -3
  16. package/src/run/complete.js +130 -33
  17. package/src/run/gates.js +37 -0
  18. package/src/run/prompt.js +20 -0
  19. package/src/run/quick-audit.js +8 -1
  20. package/src/run/scan-profile.js +29 -10
  21. package/src/run/shared.js +161 -4
  22. package/src/run/stage.js +4 -0
  23. package/src/scan-postcheck.js +25 -11
  24. package/src/stage-contract.js +39 -8
  25. package/src/stage-review.js +16 -5
  26. package/src/stages/archive.js +13 -9
  27. package/src/stages/brainstorm-auto.js +3 -2
  28. package/src/stages/brainstorm.js +13 -16
  29. package/src/stages/execute.js +4 -3
  30. package/src/stages/plan-postcheck.js +1 -1
  31. package/src/stages/plan.js +13 -0
  32. package/src/stages/propose.js +3 -2
  33. package/src/stages/quick.js +31 -31
  34. package/src/stages/scan.js +2 -1
  35. package/src/stages/status.js +2 -2
  36. package/src/stages/verify.js +19 -8
  37. package/src/task-review.js +308 -3
  38. package/src/verify-postcheck.js +353 -38
  39. package/src/worktree-apply.js +134 -67
  40. package/src/worktree.js +106 -32
  41. package/templates/prompts/verify-probes.md +13 -2
@@ -23,7 +23,7 @@ import { join, resolve, dirname } from 'node:path'
23
23
  import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
24
24
  import { randomBytes, randomUUID } from 'node:crypto'
25
25
  import { writeAtomicSync } from '../fs-atomic.js'
26
- import { resolveSpecDir, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName } from './shared.js'
26
+ import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift } from './shared.js'
27
27
  import { resolveQuickLinkedChanges } from './quick-audit.js'
28
28
  import { outputStep } from './prompt.js'
29
29
  import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
@@ -195,6 +195,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
195
195
  console.error(' 这些动作互斥,请只保留其中一个再重试。')
196
196
  process.exit(2)
197
197
  }
198
+ // scan profile flag 互斥:--quick/--standard/--deep 同时给≥2 个语义矛盾(computeScanProfile
199
+ // 只取首个命中、其余静默忽略 → agent 以为生效的其实没生效,与 STEP_ACTIONS 同类风险)。
200
+ const PROFILE_FLAGS = [
201
+ ['--quick', flags.includes('--quick'), 'quick profile(快速接入,4 份核心文档)'],
202
+ ['--standard', flags.includes('--standard'), 'standard profile(压缩步骤)'],
203
+ ['--deep', flags.includes('--deep'), 'deep profile(完整扫描)'],
204
+ ]
205
+ const activeProfiles = PROFILE_FLAGS.filter(([, v]) => v)
206
+ if (activeProfiles.length >= 2) {
207
+ console.error('❌ scan profile 参数冲突(同时只能指定一个):')
208
+ for (const [name, , desc] of activeProfiles) console.error(` ${name} — ${desc}`)
209
+ console.error(' 这三档互斥,请只保留其中一个再重试。')
210
+ process.exit(2)
211
+ }
198
212
  // 注:--non-interactive 与 --interactive 的冲突在 index.js 检测(--interactive 被 index.js
199
213
  // 作为全局 flag 吞掉、不透传到此处,故 isInteractive 在此恒为 false)。
200
214
  const waitReason = getFlagValue('--reason')
@@ -279,6 +293,17 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
279
293
  // runCommand 后续所有 .sillyspec/ 操作必须用 specBase
280
294
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
281
295
 
296
+ // 漂移提醒:cwd 祖先链 ≥2 个 .sillyspec = monorepo 多实例,当前命中的「最近」实例
297
+ // 可能不是用户意图的项目(如 cd 进被独立 scan 的子项目跑测试后忘回根)。
298
+ // 平台模式 / 显式 --spec-dir 跳过(已明确指定,无歧义)。仅提醒不阻断。
299
+ if (!platformOpts.specRoot && !specDir) {
300
+ const ancestorCount = countAncestorSpecDirs(cwd)
301
+ if (ancestorCount >= 2) {
302
+ console.warn(`⚠️ 检测到祖先链有 ${ancestorCount} 个 .sillyspec 实例(monorepo 多实例),当前使用: ${specBase}`)
303
+ console.warn(` 若意图是另一个项目:cd 回该项目根,或用 --spec-dir <根>/.sillyspec 显式指定。`)
304
+ }
305
+ }
306
+
282
307
  // 平台模式:首次接入时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)。
283
308
  // ⚠️ 同 init.js:必须保护真实资产(changes/、projects/、sillyspec.db)。
284
309
  // 只在「首次」执行一次——用 cwd 下的 .sillyspec-platform-cleaned 标记文件记录已处理,
@@ -437,7 +462,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
437
462
  '--files', '--allow-new', '--force-baseline', '--force-rescan',
438
463
  '--json', '--dir', '--help',
439
464
  '--reopen', '--from-step', '--mode',
440
- '--deep', // F1: scan-profile.js 从 argv 读 --deep,文档化(index.js help)但此前 knownFlags 漏列→被拒
465
+ '--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
441
466
  ])
442
467
  for (let i = 0; i < flags.length; i++) {
443
468
  const f = flags[i]
@@ -497,6 +522,21 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
497
522
  process.exit(2) // 用法错(execute 必须显式 --change)→ exit 2
498
523
  }
499
524
 
525
+ // worktree 副本漂移硬守卫(坑 worktree-execute-spec-drift):specBase 命中 worktree 内 .sillyspec
526
+ // checkout 副本 → 进度/产出会写分裂库。必须在 validateChangeExists 之前——副本里 change 目录真实
527
+ // 存在,存在性校验会被骗放行。覆盖 plan/execute/verify/archive(validateChangeExists 同 Set):
528
+ // 都要求 change 已存在、都被副本骗、都写 .sillyspec 资产(archive 尤甚:在副本里移动 change+commit)。
529
+ // 平台模式/显式 --spec-dir 跳过(已明确指定,与 line~285 warn / ~529 quick 守卫条件对齐)。
530
+ if (!platformOpts.specRoot && !specDir
531
+ && ['plan', 'execute', 'verify', 'archive'].includes(stageName)) {
532
+ const wt = detectWorktreeSpecDrift(specBase)
533
+ if (wt) {
534
+ console.error(`❌ ${wt.message}`)
535
+ console.error(` 当前 cwd=${cwd},命中的 spec=${specBase}。`)
536
+ process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
537
+ }
538
+ }
539
+
500
540
  // --change 变更名存在性校验(治 cwd 漂移误匹配,缺陷 execute-in-place-windows-pitfalls 坑5):
501
541
  // 必须在 pm.read/initChange 之前——initChange 会为 --change 指定的新名字创建 changes/ 目录,
502
542
  // 校验若在其后则目录已存在、永远通过。用原始 changeName(--change 参数),覆盖 plan/execute/
@@ -509,6 +549,21 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
509
549
  process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
510
550
  }
511
551
 
552
+ // quick 专用 cwd 漂移 fail-fast 守卫(坑 quick-cwd-drift-splits-specdir):
553
+ // quick 被 validateChangeExists 的 sessionId 豁免(quick-<8hex> 不在 changes/ 下),漂移时除上方
554
+ // countAncestorSpecDirs 的 warn 外无硬守卫 → 无声分裂(progress/artifact/QUICKLOG 落子项目、根
555
+ // 会话停滞)。这里补:当前 specBase 无本 session guard、但祖先链别处有 = 跨 specDir 漂移 → fail-fast。
556
+ // 平台模式/显式 --spec-dir 跳过(specBase 已明确,与 line~285 warn 条件对齐)。
557
+ if (stageName === 'quick' && changeName && /^quick-[0-9a-f]{8}$/.test(changeName)
558
+ && !platformOpts.specRoot && !specDir) {
559
+ const drift = detectQuickSessionDrift(cwd, specBase, changeName)
560
+ if (drift) {
561
+ console.error(`❌ ${drift.message}`)
562
+ console.error(` 当前 cwd=${cwd},命中的 spec=${specBase}。`)
563
+ process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
564
+ }
565
+ }
566
+
512
567
  let progress = await pm.read(cwd, changeName)
513
568
 
514
569
  if (!progress) {
@@ -884,7 +939,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
884
939
  // Auto 模式下 brainstorm 使用 artifact-first 步骤
885
940
  if (stage === 'brainstorm') {
886
941
  const existingSteps = progress.stages?.brainstorm?.steps
887
- const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '状态检查与上下文加载'
942
+ const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '进度确认与上下文加载'
888
943
  if (!isAutoModeSteps) {
889
944
  if (!progress.stages) progress.stages = {}
890
945
  progress.stages.brainstorm = {
@@ -29,6 +29,7 @@ import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
29
29
  import { printQuickAuditReview } from './quick-audit.js'
30
30
  import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry } from '../quicklog.js'
31
31
  import { getRule } from '../stage-contract-spec.js'
32
+ import { archiveDestDirName } from '../stage-contract.js'
32
33
 
33
34
  /**
34
35
  * 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
@@ -101,7 +102,8 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
101
102
  const archiveDir = join(changesDir, 'archive')
102
103
  const srcDir = join(changesDir, archiveChangeName)
103
104
  const date = new Date().toISOString().slice(0, 10)
104
- const destDir = join(archiveDir, `${date}-${archiveChangeName}`)
105
+ const destName = archiveDestDirName(date, archiveChangeName)
106
+ const destDir = join(archiveDir, destName)
105
107
 
106
108
  if (!existsSync(srcDir)) {
107
109
  console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
@@ -157,7 +159,7 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
157
159
  console.warn(`⚠️ 归档 worktree 清理失败(不阻断归档): ${e.message}`)
158
160
  }
159
161
 
160
- console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
162
+ console.log(`📦 已归档:${archiveChangeName} → archive/${destName}/`)
161
163
  return destDir
162
164
  }
163
165
  /**
@@ -767,6 +769,9 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
767
769
  source_commit_error: sourceCommit === null ? (sourceCommitError || 'unknown') : undefined,
768
770
  generated_at: new Date().toISOString(),
769
771
  schema_version: 1,
772
+ scan_profile: stageData.scanProfile
773
+ ? { mode: stageData.scanProfile.mode, reason: stageData.scanProfile.reason }
774
+ : null,
770
775
  postcheck_result_path: null,
771
776
  workflow_runs_dir: platformOpts.runtimeRoot
772
777
  ? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown', 'workflow-runs')
@@ -795,6 +800,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
795
800
  projectListParsed: stageData.scanMeta?.projectListParsed ?? null,
796
801
  manifestWritten: stageData.scanMeta?.manifestWritten ?? null,
797
802
  },
803
+ scanProfile: stageData.scanProfile || null,
798
804
  })
799
805
  printScanPostCheckResult(postResult)
800
806
 
@@ -869,7 +875,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
869
875
  // 非 platform 模式 scan 也做轻量 post-check + 结构化输出
870
876
  if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
871
877
  const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
872
- const postResult = runScanPostCheck({ cwd, specDir: null, outputText })
878
+ const postResult = runScanPostCheck({ cwd, specDir: null, outputText, scanProfile: stageData.scanProfile || null })
873
879
  printScanPostCheckResult(postResult)
874
880
  // 结构化结果写入 .sillyspec/.runtime/
875
881
  const structured = formatStructuredResult(postResult, { source_root: cwd })
@@ -18,7 +18,7 @@ import { join } from 'node:path'
18
18
  import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync } from 'node:fs'
19
19
  import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions } from './shared.js'
20
20
  import { outputStep } from './prompt.js'
21
- import { enforceDepsGate, runStageCompletionGates } from './gates.js'
21
+ import { enforceDepsGate, enforceReviewJsonGate, runStageCompletionGates } from './gates.js'
22
22
  import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact, handleExecuteWorktreeCleanup, handleScanStageCompleted } from './complete-handlers.js'
23
23
  import { stageRegistry } from '../stages/index.js'
24
24
  import { formatExecuteSummary } from '../worktree-apply.js'
@@ -174,6 +174,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
174
174
 
175
175
  // ── execute deps 验证硬门(change 2026-06-28-worktree-deps-provision)──
176
176
  await enforceDepsGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
177
+ // review.json 字段硬门(坑 review-json-field-gap):已勾 [x] task 的 review.json 必须 schema 完整,
178
+ // 提前到每次 --done 校验(而非等 Task Review Gate 在整阶段收尾才暴露,迫使用户事后批量补)。
179
+ await enforceReviewJsonGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
177
180
 
178
181
  steps[currentIdx].status = 'completed'
179
182
  steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
@@ -211,6 +214,39 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
211
214
  // execute Wave artifact(W6 Step6c 抽至 complete-handlers.js handleExecuteWaveArtifact)
212
215
  await handleExecuteWaveArtifact({ stageName, steps, currentIdx, changeName, specBase, cwd })
213
216
 
217
+ // execute 批量完成检测:plan 全勾 + 代码客观核验通过 → 剩余 step 一次性标 completed,
218
+ // 使本次 --done 直接进入阶段完成分支(治"3 Wave 做完仍逐次 +1、需重走 7 次 --done")。
219
+ // 先按 review.json pass 自动勾 plan checkbox(复用 continueStep 同源逻辑),再判批量条件。
220
+ if (stageName === 'execute' && changeName) {
221
+ const _ac = await autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts })
222
+ if (_ac.autoChecked) {
223
+ console.log(` ✅ 自动勾选 ${_ac.checkedCount} 个 task checkbox(基于 review.json pass)`)
224
+ }
225
+ if (_ac.skippedCount > 0) {
226
+ console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ 批量完成条件不满足,仍按单步推进`)
227
+ }
228
+ const _bf = await detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps })
229
+ if (_bf.batched && _bf.aligned > 0) {
230
+ console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
231
+ }
232
+
233
+ // per-task review 草稿兜底(坑 worktree-execute-apply-friction 坑2):主 agent 直接实现模式
234
+ // 无子代理 review 落盘 → Task Review Gate 报缺 review.json 阻断。每次 execute --done 跑(幂等,
235
+ // 已存在跳过),据 git diff base..head 按 allowed_paths 归属生成 cannot_verify 草稿,进 gate 前就位。
236
+ try {
237
+ const { generateTaskReviewDrafts } = await import('../task-review.js')
238
+ const _drafts = await generateTaskReviewDrafts({ changeName, cwd, platformOpts })
239
+ if (_drafts.generated > 0) {
240
+ console.log(' 📄 自动补写 ' + _drafts.generated + ' 个 per-task review.json 草稿(cannot_verify,主 agent 实现模式兜底,需 agent 复核后升级为 pass/fail)')
241
+ if (_drafts.unattributed && _drafts.unattributed.length > 0) {
242
+ console.warn(' ⚠️ ' + _drafts.unattributed.length + ' 个变更文件未归属任何 task(顺带修复/非源码),草稿未覆盖:' + _drafts.unattributed.join(', '))
243
+ }
244
+ }
245
+ } catch (e) {
246
+ console.warn(' ⚠️ per-task review 草稿生成失败(不阻断 execute 完成):' + (e && e.message ? e.message : e))
247
+ }
248
+ }
249
+
214
250
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
215
251
 
216
252
  if (nextPendingIdx === -1) {
@@ -349,6 +385,92 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
349
385
  }
350
386
 
351
387
  // ── Step 调度兄弟函数(W6 Step7b-2 从 run.js 搬入:skip/wait/continue + formatWaitHistory)──
388
+ /**
389
+ * plan.md checkbox 自动勾选:execute 完成 + 各 task review.json 双 verdict 非 fail → 自动勾选。
390
+ * 治本于"plan 全靠手动勾、易遗漏"——以 review.json pass 为客观依据勾选。供 continueStep 完成段
391
+ * 与 completeStep 的 execute 批量完成检测复用(避免两处复制)。
392
+ *
393
+ * @returns {{autoChecked:boolean, checkedCount:number, skippedCount:number, planTotal:number, planChecked:number}}
394
+ * planTotal/planChecked 在调用方于本函数之后用 readPlanCheckboxStatus 重读(勾选已落盘)。
395
+ */
396
+ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
397
+ if (stageName !== 'execute' || !changeName) {
398
+ return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
399
+ }
400
+ try {
401
+ const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
402
+ const changeDir = join(specBaseLc, 'changes', changeName)
403
+ const planPath = join(changeDir, 'plan.md')
404
+ const runtimeRoot = platformOpts?.runtimeRoot || join(specBaseLc, '.runtime')
405
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
406
+ if (!existsSync(planPath) || !existsSync(runIdFile)) {
407
+ return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
408
+ }
409
+ const executeRunId = readFileSync(runIdFile, 'utf8').trim()
410
+ const planContent = readFileSync(planPath, 'utf8')
411
+ const { readReview } = await import('../task-review.js')
412
+ let checkedCount = 0
413
+ let skippedCount = 0
414
+ const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
415
+ const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
416
+ const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
417
+ const r = readReview(reviewPath)
418
+ if (r.ok && r.review?.specVerdict !== 'fail' && r.review?.qualityVerdict !== 'fail') {
419
+ checkedCount++
420
+ return `${p1}x${p2}` // 勾选
421
+ }
422
+ skippedCount++
423
+ return match // 不勾
424
+ })
425
+ if (checkedCount > 0) {
426
+ writeFileSync(planPath, updated)
427
+ }
428
+ return { autoChecked: checkedCount > 0, checkedCount, skippedCount }
429
+ } catch {
430
+ return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
431
+ }
432
+ }
433
+
434
+ /**
435
+ * execute 批量完成检测:plan.md 所有 task checkbox 已勾 + 代码客观核验非零变更 →
436
+ * 把剩余 pending/in-progress step 一次性标 completed,使 completeStep 本次 --done 即进入阶段完成
437
+ * 分支(而非逐次 +1)。治"3 Wave 全做完仍显示未开工、需重走 7 次 --done"。
438
+ * 复用现成能力:readPlanCheckboxStatus(plan 勾选状态)+ checkExecuteCodeEvidence(代码客观核验,
439
+ * 与 doctor --align-execute-progress 同源,D-002/D-004)。仅在 stageName==='execute' && changeName 触发。
440
+ *
441
+ * 安全门:plan 零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任 plan 声明但用代码核验兜底。
442
+ * @returns {Promise<{batched:boolean, aligned:number, reason?:string}>}
443
+ */
444
+ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps }) {
445
+ if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
446
+ try {
447
+ const changeDir = join(specBase, 'changes', changeName)
448
+ const { total: planTotal, checked: planChecked } = pm.readPlanCheckboxStatus(changeDir)
449
+ if (planTotal === 0) return { batched: false, aligned: 0, reason: 'plan.md 无 task checkbox' }
450
+ if (planChecked < planTotal) {
451
+ return { batched: false, aligned: 0, reason: `plan 未全勾(${planChecked}/${planTotal})` }
452
+ }
453
+ // plan 全勾 → 代码客观核验(防 plan 被手动勾伪造导致空完成)
454
+ const { checkExecuteCodeEvidence } = await import('../stage-contract.js')
455
+ const evidence = checkExecuteCodeEvidence(cwd, changeName)
456
+ if (evidence.status === 'unchanged') {
457
+ return { batched: false, aligned: 0, reason: `代码零变更(${evidence.detail})` }
458
+ }
459
+ const now = new Date().toLocaleString('zh-CN', { hour12: false })
460
+ let aligned = 0
461
+ for (const step of steps) {
462
+ if (step.status === 'pending' || step.status === 'in-progress') {
463
+ step.status = 'completed'
464
+ step.completedAt = now
465
+ aligned++
466
+ }
467
+ }
468
+ return { batched: true, aligned }
469
+ } catch {
470
+ return { batched: false, aligned: 0 }
471
+ }
472
+ }
473
+
352
474
  function formatWaitHistory(step) {
353
475
  const answers = Array.isArray(step.waitAnswers) ? step.waitAnswers : []
354
476
  if (answers.length === 0) return null
@@ -621,38 +743,13 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
621
743
  console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
622
744
  console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
623
745
  // plan.md checkbox auto-check:execute 完成 + review.json pass → 自动勾选(治本,比警告可靠)
624
- try {
625
- const specBaseLc = platformOpts.specRoot || join(cwd, '.sillyspec')
626
- const changeDir = join(specBaseLc, 'changes', changeName)
627
- const planPath = join(changeDir, 'plan.md')
628
- const runtimeRoot = platformOpts.runtimeRoot || join(specBaseLc, '.runtime')
629
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
630
- if (existsSync(planPath) && existsSync(runIdFile)) {
631
- const executeRunId = readFileSync(runIdFile, 'utf8').trim()
632
- const planContent = readFileSync(planPath, 'utf8')
633
- const { readReview } = await import('../task-review.js')
634
- let checkedCount = 0
635
- let skippedCount = 0
636
- const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
637
- const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
638
- const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
639
- const r = readReview(reviewPath)
640
- if (r.ok && r.review?.specVerdict !== 'fail' && r.review?.qualityVerdict !== 'fail') {
641
- checkedCount++
642
- return `${p1}x${p2}` // 勾选
643
- }
644
- skippedCount++
645
- return match // 不勾
646
- })
647
- if (checkedCount > 0) {
648
- writeFileSync(planPath, updated)
649
- console.log(` ✅ 自动勾选 ${checkedCount} 个 task checkbox(基于 review.json pass)`)
650
- }
651
- if (skippedCount > 0) {
652
- console.warn(` ⚠️ ${skippedCount} 个 task 未勾(review.json 缺失/fail)→ archive 会拦。补 review 后重跑 execute --done 触发自动勾`)
653
- }
654
- }
655
- } catch {}
746
+ const _ac = await autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts })
747
+ if (_ac.autoChecked) {
748
+ console.log(` ✅ 自动勾选 ${_ac.checkedCount} 个 task checkbox(基于 review.json pass)`)
749
+ }
750
+ if (_ac.skippedCount > 0) {
751
+ console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ archive 会拦。补 review 后重跑 execute --done 触发自动勾`)
752
+ }
656
753
  }
657
754
  }
658
755
  return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
package/src/run/gates.js CHANGED
@@ -96,6 +96,39 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
96
96
  }
97
97
  process.exit(1)
98
98
  }
99
+
100
+ /**
101
+ * execute --done 硬门:已勾 [x] task 的 review.json 必须 schema 完整(坑 review-json-field-gap)。
102
+ *
103
+ * Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
104
+ * actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
105
+ * review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
106
+ * task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式。
107
+ * 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
108
+ */
109
+ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
110
+ if (stageName !== 'execute' || !changeName) return true
111
+ const runtimeRoot = platformOpts?.runtimeRoot || join(specBase, '.runtime')
112
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
113
+ const planPath = join(specBase, 'changes', changeName, 'plan.md')
114
+ if (!existsSync(runIdFile) || !existsSync(planPath)) return true
115
+ const executeRunId = readFileSync(runIdFile, 'utf8').trim()
116
+ const planContent = readFileSync(planPath, 'utf8')
117
+ const { validateCheckedTaskReviews } = await import('../task-review.js')
118
+ const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
119
+ if (result.ok) return true
120
+ if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
121
+ console.error('❌ ── review.json 字段校验阻断(本次 --done 未完成,进度未推进)──')
122
+ console.error(' 已勾选 [x] 的 task review.json 不完整(铁律:勾 checkbox 前必须先写完整 review.json):')
123
+ for (const f of result.failures) {
124
+ const kindLabel = f.kind === 'missing' ? 'review.json 不存在' : (f.kind === 'parseError' ? 'JSON 解析失败' : '字段缺失')
125
+ console.error(` • ${f.taskId}(${kindLabel}): ${f.reviewPath}`)
126
+ for (const e of f.errors) console.error(` - ${e}`)
127
+ }
128
+ console.error(' 修复:补全上述 review.json 字段后重跑 execute --done。')
129
+ process.exit(1)
130
+ }
131
+
99
132
  /**
100
133
  * 阶段完成校验失败时回滚状态。
101
134
  *
@@ -183,6 +216,10 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
183
216
  const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
184
217
  const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: platformOpts?.runtimeRoot })
185
218
  printVerifyParityCheck(parityCheck)
219
+ // ── 删除探针(advisory,不阻断):切斯特顿栅栏护栏,用 git 事实对账静默删除代码 ──
220
+ const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
221
+ const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
222
+ printVerifyDeletionCheck(deletionCheck)
186
223
  console.log('\n✅ 验证通过,下一步:sillyspec run archive')
187
224
  }
188
225
 
package/src/run/prompt.js CHANGED
@@ -517,6 +517,26 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
517
517
  }
518
518
  }
519
519
 
520
+ // archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
521
+ // 替代「机械数 plan.md checkbox」(checkbox 依赖 autoCheckPlanFromReviews 回填,runId marker /
522
+ // review 缺失时回填静默 no-op,会停在未勾态导致完成度失真 → archive 误判「全未完成」)。
523
+ // summarizeTaskCompletion 内部已 fail-safe 降级(无 plan / 无 runId → checkbox 统计 + 标注 source),
524
+ // 此处仅兜底注入异常,绝不阻断 archive。
525
+ if (stageName === 'archive' && promptText.includes('{TASK_COMPLETION_REPORT}')) {
526
+ try {
527
+ const { summarizeTaskCompletion } = await import('../task-review.js')
528
+ const tcrSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
529
+ const tcrRuntimeRoot = platformOpts?.runtimeRoot || join(tcrSpecBase, '.runtime')
530
+ const tcrChangeDir = changeName ? join(tcrSpecBase, 'changes', changeName) : null
531
+ const summary = tcrChangeDir
532
+ ? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
533
+ : null
534
+ promptText = promptText.split('{TASK_COMPLETION_REPORT}').join(summary ? summary.report : '(无法计算完成度:变更目录缺失)')
535
+ } catch (e) {
536
+ promptText = promptText.split('{TASK_COMPLETION_REPORT}').join('(完成度计算异常:' + e.message + ',请手动核对 plan.md 与 .runtime/execute-runs/*/tasks/task-NN/review.json)')
537
+ }
538
+ }
539
+
520
540
  // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
521
541
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
522
542
  const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
@@ -34,7 +34,14 @@ export function printQuickAuditReview(review) {
34
34
  console.warn(` - ${r}`)
35
35
  }
36
36
  } else {
37
- console.log(`\n✅ quick 变更边界审计 — SAFE (变更 ${review.changedFiles.length} 个文件)`)
37
+ // 区分「本轮新增」(changedFiles,已扣前序 baseline)vs「累计暂存」(stagedTotal,全部未提交)。
38
+ // 仅当存在前序 baseline 残留(累计 > 本轮新增)时才追加括注,避免普通场景冗余。
39
+ const stagedTotal = typeof review.stagedTotal === 'number' ? review.stagedTotal : review.changedFiles.length
40
+ if (stagedTotal > review.changedFiles.length) {
41
+ console.log(`\n✅ quick 变更边界审计 — SAFE (本轮新增变更 ${review.changedFiles.length} 个文件;累计暂存 ${stagedTotal} 个,含前序 baseline ${stagedTotal - review.changedFiles.length} 个未计入本轮)`)
42
+ } else {
43
+ console.log(`\n✅ quick 变更边界审计 — SAFE (本轮新增变更 ${review.changedFiles.length} 个文件)`)
44
+ }
38
45
  }
39
46
  }
40
47
 
@@ -15,23 +15,36 @@ import { safeGit } from './shared.js'
15
15
  /**
16
16
  * 估算源码规模(文件数 + 字节数)—— 跨平台 Node 遍历,替代 Unix `find`(原生 Windows 无 find,
17
17
  * 原 find 失效会让所有 Windows 项目永远 fallback 到 standard profile)。
18
- * 扩展名与排除目录与原 find 命令逐字一致;maxFiles 封顶避免超大仓库遍历过久
19
- * (超限时 fileCount 已远超 quick/standard 阈值,不影响 profile 判定)。
18
+ * 扩展名限定源码后缀;skipDirs 排除依赖/构建产物/覆盖率等非源码目录;maxFiles 封顶避免超大仓库遍历过久
19
+ * (超限时 fileCount 已远超 quick/standard 阈值,不影响 profile 判定);maxDepth 兜底防极深嵌套产物。
20
20
  */
21
- export function estimateSourceSize(cwd, maxFiles = 5000) {
21
+ export function estimateSourceSize(cwd, maxFiles = 5000, maxDepth = 6) {
22
22
  const sourceExts = new Set(['.js','.ts','.tsx','.py','.java','.go','.rs','.rb','.php','.c','.cpp','.h','.jsx','.vue','.svelte'])
23
- const skipDirs = new Set(['node_modules','.git','dist','build','__pycache__','.sillyspec','.claude'])
23
+ // skipDirs:排除依赖、版本控制、构建产物、覆盖率、临时目录等非源码目录。
24
+ // 原清单只排了 node_modules/.git/dist/build 等,漏了 .next/.nuxt/coverage/.svelte-kit 等构建产物,
25
+ // 导致带构建产物的小项目被产物里的海量 .js 拉高 fileCount/sourceBytes → 误判 deep。
26
+ const skipDirs = new Set([
27
+ 'node_modules', '.git', 'dist', 'build', 'out', 'target',
28
+ '__pycache__', '.venv', 'venv',
29
+ '.next', '.nuxt', '.output', '.svelte-kit', '.astro', '.turbo', '.parcel-cache',
30
+ 'coverage', '.nyc_output', '.vitest', '.cache',
31
+ 'vendor', 'bower_components',
32
+ 'dist-types',
33
+ '.sillyspec', '.claude',
34
+ 'tmp', 'temp', '.tmp', 'logs',
35
+ ])
24
36
  let fileCount = 0
25
37
  let sourceBytes = 0
26
- const stack = [cwd]
38
+ // stack 存 [dir, depth]:maxDepth 兜底,防极深嵌套的产物/生成目录在 skipDirs 漏网时被全遍历。
39
+ const stack = [[cwd, 0]]
27
40
  while (stack.length) {
28
- const dir = stack.pop()
41
+ const [dir, depth] = stack.pop()
29
42
  let entries
30
43
  try { entries = readdirSync(dir, { withFileTypes: true }) }
31
44
  catch { continue }
32
45
  for (const e of entries) {
33
46
  if (e.isDirectory()) {
34
- if (!skipDirs.has(e.name)) stack.push(join(dir, e.name))
47
+ if (!skipDirs.has(e.name) && depth < maxDepth) stack.push([join(dir, e.name), depth + 1])
35
48
  } else if (e.isFile() && sourceExts.has(extname(e.name))) {
36
49
  fileCount++
37
50
  try { sourceBytes += statSync(join(dir, e.name)).size } catch {}
@@ -49,8 +62,14 @@ export function estimateSourceSize(cwd, maxFiles = 5000) {
49
62
  * deep: 大项目或 --deep → 完整流程
50
63
  */
51
64
  export function computeScanProfile(cwd, platformOpts) {
52
- // --deep 标志强制 deep
65
+ // 显式 profile flag 优先于自动判定(三档互斥由 command.js 检测;此处取首个命中即可)
53
66
  const flags = process.argv.slice(2)
67
+ if (flags.includes('--quick')) {
68
+ return { mode: 'quick', reason: '用户指定 --quick', maxAgentCalls: 0, maxDocs: 5 }
69
+ }
70
+ if (flags.includes('--standard')) {
71
+ return { mode: 'standard', reason: '用户指定 --standard', maxAgentCalls: 1, maxDocs: 8 }
72
+ }
54
73
  if (flags.includes('--deep')) {
55
74
  return { mode: 'deep', reason: '用户指定 --deep', maxAgentCalls: 4, maxDocs: 99 }
56
75
  }
@@ -115,7 +134,7 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
115
134
  status: 'pending',
116
135
  prompt: `## Quick Scan — 核心文档生成
117
136
 
118
- 项目规模较小(quick profile),请一次性生成所有核心文档。
137
+ 当前为 quick profile(小项目自动判定 或 用户显式 --quick),一次性生成核心文档用于快速接入。
119
138
 
120
139
  ### 操作
121
140
  1. 读取项目结构和关键文件(package.json / pyproject.toml / README / 入口文件)
@@ -126,7 +145,7 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
126
145
  - **STRUCTURE.md** — 目录树 + 模块说明
127
146
  3. 如发现子项目,注册到 \`{PROJECTS_ROOT}/\` 下
128
147
 
129
- 每份文档头必须包含 frontmatter:\`author\` 和 \`created_at\`。
148
+ 每份文档 frontmatter 必须包含:\`author\`、\`created_at\`、\`scan_depth: quick\`(标记快速接入的浅层版本;后续深度扫描 --deep 会识别此标记并覆盖升级为完整文档)。
130
149
 
131
150
  ### ⛔ 硬约束
132
151
  - **严禁使用子代理(Agent/Task 工具)。** 所有文档在一个 turn 内完成。