sillyspec 3.29.5 → 3.30.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 (110) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
  5. package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
  6. package/AGENTS.md +38 -20
  7. package/SKILL.md +9 -3
  8. package/assets/command-cards/flow.md +32 -0
  9. package/assets/command-cards/run-brainstorm.md +1 -1
  10. package/assets/command-cards/run-quick.md +8 -25
  11. package/package.json +2 -1
  12. package/packages/dashboard/server/executor.js +2 -1
  13. package/packages/dashboard/server/parser.js +7 -7
  14. package/r7-test-env/README-R7-TEST.md +93 -0
  15. package/r7-test-env/demo-project/lib/calc.js +6 -0
  16. package/r7-test-env/demo-project/package.json +1 -0
  17. package/r7-test-env/demo-project/test.js +10 -0
  18. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  19. package/r7-test-env/sillyspec-r7.cmd +3 -0
  20. package/round4/account.mjs +18 -5
  21. package/round4/b1-analyze-task08.mjs +40 -0
  22. package/round4/b1-b1-target-validation.md +32 -0
  23. package/round4/b3a-prompt-audit-result.md +66 -0
  24. package/round4/b3a-prompt-audit.mjs +89 -0
  25. package/round4/optimization-plan.md +165 -0
  26. package/round4/r4-final-report.html +347 -0
  27. package/round4/r5-scenario-matrix.md +46 -0
  28. package/src/change-list.js +9 -0
  29. package/src/classify-change.js +6 -0
  30. package/src/command-cards.js +2 -1
  31. package/src/commit-guard.js +2 -0
  32. package/src/config-schema.js +64 -12
  33. package/src/db.js +35 -4
  34. package/src/decision-distill.js +9 -3
  35. package/src/docs-check.js +1 -1
  36. package/src/doctor-diagnostics.js +44 -2
  37. package/src/flow-draft.js +748 -0
  38. package/src/flow-parity.js +236 -0
  39. package/src/flow-review.js +184 -0
  40. package/src/flow.js +1492 -0
  41. package/src/foreign-declared.js +17 -1
  42. package/src/fr-index.js +396 -22
  43. package/src/fs-atomic.js +5 -1
  44. package/src/git-helper.js +2 -2
  45. package/src/greenfield-bootstrap.js +98 -0
  46. package/src/handoff.js +120 -7
  47. package/src/hunk-attribution.js +191 -0
  48. package/src/index.js +570 -16
  49. package/src/knowledge-classify.js +47 -1
  50. package/src/knowledge-digest.js +162 -0
  51. package/src/knowledge-quicklog.js +246 -0
  52. package/src/machine-draft.js +128 -0
  53. package/src/machine-interface.js +107 -5
  54. package/src/mcp-server.js +1 -0
  55. package/src/module-resolve.js +3 -1
  56. package/src/preview-progress.js +160 -0
  57. package/src/progress/change-registry.js +11 -3
  58. package/src/progress/shared.js +3 -2
  59. package/src/progress.js +56 -13
  60. package/src/quicklog.js +44 -6
  61. package/src/redomain.js +147 -0
  62. package/src/review-material-pack.js +204 -1
  63. package/src/run/archive-distill.js +4 -2
  64. package/src/run/bg-sync.js +67 -0
  65. package/src/run/command.js +123 -10
  66. package/src/run/complete-handlers.js +327 -72
  67. package/src/run/complete.js +350 -71
  68. package/src/run/gate-snapshot-ledger.js +213 -0
  69. package/src/run/gate-snapshot.js +250 -34
  70. package/src/run/gates.js +370 -233
  71. package/src/run/green-cache.js +123 -0
  72. package/src/run/junction-rm.js +70 -0
  73. package/src/run/next.js +14 -0
  74. package/src/run/prompt.js +185 -55
  75. package/src/run/quick-audit.js +185 -12
  76. package/src/run/shared.js +120 -3
  77. package/src/run/stage.js +182 -216
  78. package/src/run/test-ledger.js +264 -0
  79. package/src/run/verify-quality-scan.js +84 -7
  80. package/src/scope-audit.js +6 -1
  81. package/src/sentinel-assertions.js +114 -0
  82. package/src/setup.js +3 -3
  83. package/src/spec-sync.js +41 -1
  84. package/src/stage-contract.js +26 -8
  85. package/src/stage-review-checklist.js +2 -0
  86. package/src/stage-review.js +7 -2
  87. package/src/stage-templates.js +157 -0
  88. package/src/stages/archive.js +2 -0
  89. package/src/stages/brainstorm.js +24 -106
  90. package/src/stages/execute.js +221 -84
  91. package/src/stages/knowledge.js +74 -1
  92. package/src/stages/plan-postcheck.js +352 -1
  93. package/src/stages/plan.js +23 -1
  94. package/src/stages/quick.js +10 -2
  95. package/src/stages/verify.js +4 -2
  96. package/src/sync.js +81 -23
  97. package/src/task-done.js +181 -0
  98. package/src/task-review.js +8 -55
  99. package/src/test-bindings.js +549 -0
  100. package/src/timeline.js +218 -0
  101. package/src/ui-visual.js +175 -0
  102. package/src/verify-draft.js +214 -0
  103. package/src/verify-postcheck.js +860 -158
  104. package/src/verify-probes.js +59 -3
  105. package/src/watcher.js +1031 -0
  106. package/src/worktree-apply.js +44 -4
  107. package/src/worktree-deps.js +22 -8
  108. package/src/worktree-policy.js +42 -0
  109. package/src/worktree.js +102 -15
  110. package/templates/agents-instruction.md +37 -18
package/src/run/stage.js CHANGED
@@ -17,14 +17,12 @@
17
17
  * - 'child_process'(execSync)裸模块名不变
18
18
  */
19
19
  import { join, dirname } from 'node:path'
20
- import { existsSync, readdirSync, readFileSync, mkdirSync } from 'node:fs'
20
+ import { existsSync, readdirSync, readFileSync, mkdirSync, rmSync, unlinkSync } from 'node:fs'
21
21
  import { writeAtomicSync } from '../fs-atomic.js'
22
- import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown, predictProtectedQuickFiles, mergeQuickBoundaryFiles, detectEmptyShellQuickSessions } from './shared.js'
22
+ import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown, predictProtectedQuickFiles, mergeQuickBoundaryFiles, readStageBurst, STAGE_BURST_STAGES, readStageWall, STAGE_WALL_STAGES } from './shared.js'
23
23
  import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
24
24
  import { executeProgressConfirm } from './progress-confirm.js'
25
25
  import { outputStep, collectStageWaitHistory } from './prompt.js'
26
- import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
27
- import { createHash } from 'node:crypto'
28
26
  import { checkTransition } from '../stage-contract.js'
29
27
  import { AUXILIARY_STAGES } from '../constants.js'
30
28
  import { completeStageGates } from './gates.js'
@@ -45,6 +43,37 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
45
43
  }
46
44
  }
47
45
 
46
+ // ── 阶段墙 hard 门(R16 减负批次 A 相位,2026-09-24;R16-b 实证修正 2026-09-25)──
47
+ // 拦「前驱刚收口、未执行 handoff 切割、目标阶段首次进入」的硬续形态。三个放行口:
48
+ // ①sillyspec handoff 已在前驱收口后执行(ledger.handoffAt > ledger.at——handoff 协议要求
49
+ // 新会话保持同一 SILLYSPEC_SESSION_ID,SESSION_ID 相同不是硬续证据,切割凭证=handoff 动作);
50
+ // ②目标阶段已有 stage 数据(reopen/复跑重入——保住 verify FAIL→execute --reopen 修复回路);
51
+ // ③--same-session 显式逃生口。fail-open:ledger 缺失/读失败/会话标识缺省 → 放行。
52
+ if (STAGE_WALL_STAGES.includes(stageName) && changeName && !(quickOpts && quickOpts.sameSession)) {
53
+ try {
54
+ if ((await readStageWall(cwd)) === 'hard') {
55
+ const curSession = process.env.SILLYSPEC_SESSION_ID || ''
56
+ const wallStageData = progress.stages && progress.stages[stageName]
57
+ const firstEntry = !wallStageData || !Array.isArray(wallStageData.steps) || wallStageData.steps.length === 0
58
+ const wallLedgerPath = join(specBase, '.runtime', `stage-session-ledger-${changeName}.json`)
59
+ if (curSession && firstEntry && existsSync(wallLedgerPath)) {
60
+ const wallLedger = JSON.parse(readFileSync(wallLedgerPath, 'utf8'))
61
+ const { isStageWallBlocked } = await import('./shared.js')
62
+ if (isStageWallBlocked({ ledger: wallLedger, curSession, stageName, stageHasData: !firstEntry })) {
63
+ console.error(``)
64
+ console.error(`⛔ 阶段墙(stage.wall=hard):本变更刚在会话标识 ${curSession} 下收口 ${wallLedger.lastStage},${stageName} 须在新会话执行。`)
65
+ console.error(` 单会话历史重放税实测:R15 execute 段均轮 263K 输入(断崖拆会话后回落 120-170K)——${stageName} 的全部输入在盘上(design/plan/tasks/review),新会话零背景可续跑。`)
66
+ console.error(` 动作:`)
67
+ console.error(` 1. sillyspec handoff --change ${changeName} # 生成交接块(即切割凭证,生成后墙自动放行)`)
68
+ console.error(` 2. 新会话粘贴交接块 → sillyspec run ${stageName} --change ${changeName}`)
69
+ console.error(` 逃生口:确认要同会话硬续(应急/平台编排)→ 加 --same-session 重跑本命令。`)
70
+ process.exit(1)
71
+ }
72
+ }
73
+ }
74
+ } catch { /* fail-open:任何读取/解析失败放行,行为与无墙一致 */ }
75
+ }
76
+
48
77
  // execute 阶段启动前检查审批
49
78
  if (stageName === 'execute' && !skipApproval) {
50
79
  const approval = await checkApproval(cwd, changeName, platformOpts)
@@ -335,6 +364,9 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
335
364
  // (D-003@v1:顶层 quickGuard 不跨进程持久化;agent 在 step 间用 `run quick` 取下一步
336
365
  // prompt 时每个新进程都进此块,按文件判幂等才不会重复分配 ql-ID / 重复写条目)。
337
366
  if (stageName === 'quick') {
367
+ // quick 通道直接退役(2026-09-25-quick-channel-retire,接替 thin-default-flip 的第 2 步横幅劝导):
368
+ // 新会话一律硬拒(见下方 !existingGuard 分支),在途会话(guard.json 存在)照常续跑/--done/--cancel
369
+ // 收尾——--done/--cancel 不经 runStage,本门只拦「新会话创建」,拒绝发生在任何写入之前(零副作用)。
338
370
  // 受保护文件预告打印(坑 quick-protected-late-hint,2026-08-28 用户实证:scan 类文档
339
371
  // ARCHITECTURE/CONCERNS 属受保护基线,--files 声明了照样拦、必须 --force-baseline——
340
372
  // 设计合理但提示太晚。step1 即预告哪些声明文件会触发拦截,省一轮跑到 --done 才发现的往返)
@@ -353,6 +385,8 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
353
385
  if (existsSync(guardFile)) existingGuard = JSON.parse(readFileSync(guardFile, 'utf8'))
354
386
  } catch {}
355
387
  if (existingGuard) {
388
+ // 在途会话提示(每次渲染一行,防 agent 把后续新工作又开成 quick):本会话收尾不受退役影响
389
+ console.log(`ℹ️ quick 通道已退役——本会话为升级前在途会话,可继续收尾(--done / --cancel);新工作走轻量变更 flow start。`)
356
390
  // 跨进程重入:复用已分配的 ql-ID,跳过 baseline 重捕与分配(幂等)
357
391
  progress.quickGuard = existingGuard
358
392
  // 中途追加边界:会话中途发现要改启动声明外的文件,此前 resume 会把 --files 静默丢弃、
@@ -376,167 +410,31 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
376
410
  }
377
411
  }
378
412
  } else {
379
- // baseline 采集用 safeGit(带 -c safe.directory,避免 linked worktree/容器异 uid/Windows 挂载点
380
- // 下裸 `git status` 抛错)。safeGit 已消除 safe.directory 类失败;若仍失败(真非 git 目录等),
381
- // 不硬阻断 quick 启动(平台模式/非 git 目录仍需渲染 step prompt),但 fail-visible:大声 warn +
382
- // baseline 置空。--done 时 auditQuickCompletion 的 safeGit 同样失败 → blocked,故不存在「静默
383
- // 完成」路径(multi-agent-review Q3)。
384
- const statusResult = safeGit(cwd, ['status', '--porcelain'], { trim: false })
385
- if (statusResult.error) {
386
- console.warn(`⚠️ quick baseline 采集失败(git status): ${statusResult.error}`)
387
- console.warn(` baseline 置空;--done 审计将因 git 不可用而阻断(无静默完成路径)。`)
388
- console.warn(` 排查:仓库 safe.directory 配置 / 非 git 目录。`)
389
- }
413
+ // quick 通道直接退役(2026-09-25-quick-channel-retire):新会话硬拒 exit 1——本 else 分支
414
+ // 原是 ql-ID 分配 / guard 落盘 / QUICKLOG 条目写入发生处,拒绝置于所有写入之前(零副作用)。
415
+ // 判据与 D-003 幂等判据同源(guard.json 文件存在性),在途会话走上方 existingGuard 分支不受影响。
416
+ // 注:CLI 进程入口的新会话已被 index.js refuseRetiredQuickFreshStart 预门拦下,本门兜底
417
+ // 进程内直调 runCommand 的入口(测试/编程调用)与 --change 指向不存在会话的形态。
418
+ console.error(`❌ quick 通道已退役,无法启动 quick 会话(若带 --change 则该会话不存在或已清理)。`)
419
+ console.error(` 新工作请走轻量变更:sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"`)
420
+ console.error(` 升级前进行中的 quick 会话仍可收尾:sillyspec run quick --change <会话ID> 续跑 / --done 收口 / --cancel 取消。`)
421
+ // 进程内路径幻影残留清理(best-effort):CLI 入口已被 index.js 预门拦下;进程内直调
422
+ // runCommand 的新会话形态在 command.js 已生成 sid + owner.json + current-quick-run-id +
423
+ // DB 活跃进度行。guard 从未落盘——清掉本 sid 的空会话目录、指向本 sid 的共享指针与 DB
424
+ // 幻影行(防污染 listChanges / 多活跃判定),防 --done fallback 命中幻影。保护:guard.json
425
+ // 存在(含损坏)绝不清目录/不注销(损坏可手修恢复,拒绝但保数据);共享指针只在内容
426
+ // 等于本 sid 时才动,他者会话标记不碰。
390
427
  try {
391
- const gitStatus = statusResult.value || ''
392
- // 记录全部预存脏文件(含 untracked + .sillyspec/ 路径)。quick 会话期间自身写入的元数据
393
- // (quicklog/.runtime/modules/_module-map 等)由 auditQuickCompletion 的 isQuickMetadata 精确豁免,
394
- // 不需要这里粗放过滤 .sillyspec/——旧过滤致预存 untracked .sillyspec/changes/ 不进 baseline,
395
- // 却在 audit 被当「危险(.sillyspec/)+新增」误判永久 blocked(ql-20260713-002-7628 修复)。
396
- const baselineFiles = gitStatus
397
- .split('\n').filter(Boolean)
398
- .map(line => parsePorcelainPath(line))
399
- .filter(Boolean)
400
- const allowedFiles = quickOpts?.quickFiles || []
401
- const allowNew = quickOpts?.isAllowNew || false
402
- const allowDelete = quickOpts?.isAllowDelete || false
403
- const forceBaseline = quickOpts?.isForceBaseline || false
404
- const linkedChanges = Array.isArray(quickOpts?.linkedChanges) ? quickOpts.linkedChanges : []
405
- // 受保护文件预告(坑 quick-protected-late-hint):会话起步(step1)即点破,
406
- // 判定与 --done 审计危险门同口径(predictProtectedQuickFiles)
407
- const protectedPreview = predictProtectedQuickFiles(allowedFiles, { linkedChanges, forceBaseline })
408
- if (protectedPreview.length > 0) warnProtectedQuickFiles(protectedPreview)
409
- // 任务描述驱动预检(2026-09-09 刀批,轮次经济学 §3.4):未预声明 --files 且未带
410
- // forceBaseline 时,用当前脏文件 × 任务描述 token 粗匹配猜可能触及的文件,命中
411
- // 受保护面即起步点破——省「--done 撞危险门 → 带 --force-baseline 重跑」一整轮。
412
- // 启发式 advisory:猜错零代价(误报忽略),猜中省一轮;精确判定仍归 --done 审计。
413
- if (protectedPreview.length === 0 && !forceBaseline) {
414
- try {
415
- const desc = quickOpts?.taskDescription || ''
416
- if (desc) {
417
- const dirty = String(safeGit(cwd, ['status', '--porcelain', '--untracked-files=all']).value || '')
418
- .split('\n').map(l => l.slice(3).trim().split(' -> ').pop() || '')
419
- .map(pp => pp.replace(/^"|"$/g, '').replace(/\\/g, '/'))
420
- .filter(pp => pp && !pp.startsWith('.sillyspec/'))
421
- const tokens = desc.toLowerCase().split(/[\s,,。;;::/\\]+/).filter(t => t.length >= 3)
422
- const guessed = dirty.filter(pp => tokens.some(t => pp.toLowerCase().includes(t) || t.includes(pp.split('/').pop().replace(/[.][a-z]+$/, '').toLowerCase())))
423
- const guessedProtected = predictProtectedQuickFiles(guessed, { linkedChanges, forceBaseline })
424
- if (guessedProtected.length > 0) {
425
- console.warn(`⚠️ 任务描述可能触及受保护文件(启发式预检,精确判定在 --done):${guessedProtected.slice(0, 3).join('、')}${guessedProtected.length > 3 ? ' …' : ''}`)
426
- console.warn(' 若确要改它们:重启 quick 带 --force-baseline 预声明,省 --done 被拦重跑一轮;误报忽略即可。')
427
- }
428
- }
429
- } catch { /* 预检 fail-open */ }
430
- }
431
- // CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
432
- const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
433
- // 标题回退:启动 quick 不带 --input 时,从关联变更的 proposal/design 提取语义标题,
434
- // 避免 QUICKLOG/tasks.md 落 (quick 任务) 占位(deriveTitleFromLinkedChange 读不到则空→sanitizeDesc 回退占位)。
435
- let quickDesc = quickOpts?.taskDescription || ''
436
- if (!quickDesc && linkedChanges.length > 0) {
437
- quickDesc = deriveTitleFromLinkedChange(specBase, linkedChanges[0])
438
- }
439
- const { qlId } = await allocateQuicklogEntry(specBase, gitUser, {
440
- description: quickDesc,
441
- linkedChanges,
442
- allowedFiles,
443
- // 他者会话 guard 预留让位(坑 ql-id-double-occupancy):与 guard 落盘同源目录
444
- // (Q4 单一解析,平台模式 runtimeRoot 分裂时也对齐)
445
- sessionsDir: resolveQuickSessionsDir(platformOpts, specBase),
446
- })
447
- progress.quickGuard = {
448
- sessionId: changeName,
449
- // 锚定创建时的 specBase(坑 quick-cwd-drift-splits-specdir):sessionId 全局复用但
450
- // guard 按 specDir 落盘,记下归属让漂移可自证(detectQuickSessionDrift 靠目录定位即可,
451
- // 此字段供诊断/未来一致性校验)。
452
- specDir: specBase,
453
- name_zh: '快速任务守卫',
454
- baselineCommit: safeGit(cwd, ['rev-parse', 'HEAD']).value,
455
- baselineFiles,
456
- allowedFiles,
457
- // task-01: 录每个 allowedFile 内容 sha256,供 --done auditQuickCompletion 检测同文件并发
458
- // (allowedFile 在 baseline 且当前 hash ≠ 此值 = 我也改了 → 同文件并发 warn)。文件不存在/读失败跳过。
459
- allowedFilesHash: Object.fromEntries(allowedFiles.flatMap(f => {
460
- try { return [[f, createHash('sha256').update(readFileSync(join(cwd, f))).digest('hex')]] }
461
- catch { return [] }
462
- })),
463
- allowNew,
464
- allowDelete,
465
- forceBaseline,
466
- linkedChanges,
467
- // 机器自动关联溯源(坑 quick-single-change-auto-link):--done 归档闸对仅被自动
468
- // 关联(信号命中猜测,非 --linked-changes 显式协作声明)的变更不触发轻量归档
469
- linkedChangesAuto: Array.isArray(quickOpts?.linkedChangesAuto) ? quickOpts.linkedChangesAuto : [],
470
- quicklogId: qlId,
471
- // 刀①(2026-09-08):启动 --input 的任务描述落 guard——prompt.js 渲染 step1 时据此做
472
- // 模块上下文关键词匹配(changeName 是 quick-<hash> 无语义,step.prompt 全文噪音大)
473
- taskDescription: quickDesc,
474
- startedAt: new Date().toISOString(),
428
+ if (!existsSync(guardFile) && /^quick-[0-9a-f]{8}$/.test(changeName)) {
429
+ // 形态门(独立评审 P2 清偿):清理/注销只对 quick 会话 id 形态执行——防任何把
430
+ // 真实变更名带进本分支的路径误注销其 DB 行;非 sid 形态只拒绝不清理
431
+ if (existsSync(sessionGuardDir)) rmSync(sessionGuardDir, { recursive: true, force: true })
432
+ try { pm.unregisterChange(cwd, changeName) } catch { /* 行不存在等忽略 */ }
475
433
  }
476
- // 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
477
- // (D-002:按 session 存,多会话各自 guard 不互覆盖。runStage 作用域内 sessionId == changeName == quick-<uuid8>,见 §4.4/4.5)
478
- mkdirSync(sessionGuardDir, { recursive: true })
479
- writeAtomicSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
480
- const parts = [`${baselineFiles.length} 个已有脏文件`]
481
- if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
482
- if (allowNew) parts.push('允许新增文件')
483
- if (allowDelete) parts.push('允许删除文件')
484
- console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
485
- // 归属点名(2026-09-11 用户实证:--done 审计的 allowedFiles 自动推断可漏实改文件,
486
- // 被拦后与危险文件混推 --force-baseline。启动未显式 --files 时,把工作区 src/test 前缀
487
- // 脏文件点名让用户当场确认归属——预声明即精确边界,免去收尾被拦一轮)。
488
- if (allowedFiles.length === 0 && Array.isArray(baselineFiles)) {
489
- const declCandidates = baselineFiles
490
- .map((p) => String(p).split('\\').join('/'))
491
- .filter((p) => p.startsWith('src/') || p.startsWith('test/'))
492
- if (declCandidates.length > 0) {
493
- console.log(`👀 工作区已有 ${declCandidates.length} 个 src/test 脏文件(本会话如会改到其中部分,建议现在声明边界;并行会话的他者改动勿声明):`)
494
- for (const p of declCandidates.slice(0, 8)) console.log(` - ${p}`)
495
- if (declCandidates.length > 8) console.log(` … 共 ${declCandidates.length} 个`)
496
- console.log(` 声明:sillyspec run quick --files <a.js,b.js> --change ${changeName}(恢复会话追加边界)`)
497
- }
498
- }
499
- console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
500
- // 空壳会话探测(坑 quick-duplicate-empty-shell,2026-09-03 用户实证:一次输出被吞 →
501
- // agent 误判失败重跑 → 重复空壳会话,此前只能手工 reset + 删骨架)。新会话起步时点名
502
- // 他者「已启动但零步骤完成」的会话 + 给官方清理口 --cancel(QUICKLOG 翻已取消 + 挂载行
503
- // 移除)。advisory 不阻断:真在用的并行会话不受影响(有完成步骤即不列)。
504
- try {
505
- const shells = detectEmptyShellQuickSessions(platformOpts, specBase, changeName, pm, cwd)
506
- if (shells.length > 0) {
507
- console.warn('')
508
- console.warn(`🧹 检测到 ${shells.length} 个疑似空壳 quick 会话(已启动但零步骤完成——常见于输出中断后误重启的残留):`)
509
- for (const s of shells) console.warn(` - ${s.sessionId}(启动于 ${s.startedAt})`)
510
- console.warn(` 清理:sillyspec run quick --cancel --change <会话ID>;若确在用请忽略本提示。`)
511
- }
512
- } catch { /* fail-open:探测失败不打扰 quick 启动 */ }
513
- // 缺 --input 且关联变更无可提取标题 → 条目落「(quick 任务)」占位标题,平台「快速修复」列表
514
- // 默认隐藏进行中占位(task-06 口径),长会话全程不可见、语义标题要到最终 --done 才回填。
515
- // 此刻会话刚起步零沉没成本,提示放弃重启带 --input 是最便宜的自愈点。
516
- if (!quickDesc) {
517
- console.warn('')
518
- console.warn('⚠️ 本次 quick 未带 --input:QUICKLOG 条目将以占位标题「(quick 任务)」落盘')
519
- console.warn(' 平台「快速修复」列表默认隐藏进行中的占位条目,语义标题要到最终 --done 才回填。')
520
- console.warn(' 长任务建议放弃本会话、带一句话任务描述重启(旧会话 run quick --reset --change <sessionId> 重置):')
521
- console.warn(' sillyspec run quick --input "<一句话任务描述>" [--linked-changes <a,b>] [--files <...>]')
522
- }
523
- // 回填 DB changes 行的 title + quicklog_id,让 quick-<hex> 可读、DB↔QUICKLOG 可对账。
524
- // title 用 quickDesc(任务描述或关联变更标题)经 sanitizeDesc 压一行限长,与 QUICKLOG 条目标题同源。
525
- try {
526
- pm.updateChangeMeta(cwd, changeName, { title: sanitizeDesc(quickDesc), quicklogId: qlId });
527
- } catch { /* 回填失败不阻断 quick 启动 */ }
528
- pm._write(cwd, progress, changeName)
529
- // ql-20260819-009:起步即推「进行中」占位条目到平台。runStage 前段三处 triggerSync
530
- // (autoDetectChange / currentStage 切换 / stale 复位)全在本块之前执行——「进行中」骨架
531
- // 此前要等第一次 --done 才上平台,平台快速修复列表存在起步盲窗。此刻骨架已分配 + 进度已
532
- // 落盘,补一次 triggerSync 立即推 QUICKLOG(quick-<hex8> 会话自动降级 syncSpecTreeOnly
533
- // 只推 spec 树;未连接平台静默跳过、8s 熔断 best-effort,均不阻断 quick 启动)。平台派发
534
- // (claim)模式起步同样走 runStage 本块,一处插入全覆盖;guard 已存在的跨进程重入走上方
535
- // existingGuard 分支跳过本块,条目已在平台,不重复推。
536
- triggerSync(cwd, changeName, platformOpts)
537
- } catch (e) {
538
- console.warn(`⚠️ baseline 记录失败: ${e.message}`)
539
- }
434
+ const marker = join(resolveRuntimeRoot(platformOpts, specBase), 'current-quick-run-id')
435
+ if (existsSync(marker) && readFileSync(marker, 'utf8').trim() === changeName) unlinkSync(marker)
436
+ } catch { /* 清理失败不掩盖拒绝语义 */ }
437
+ process.exit(1)
540
438
  }
541
439
  }
542
440
 
@@ -572,49 +470,19 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
572
470
 
573
471
  const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
574
472
  if (defSteps && defSteps[currentIdx]) {
473
+ // ── burst 渲染分支(2026-09-22-stage-burst-fold D-002/D-006/D-008,FR-02)──
474
+ // 白名单主阶段 + readStageBurst 开启时走单趟折叠渲染:noAI 步就地 CLI 执行,AI 步逐个
475
+ // 调既有 outputStep 一次下发全部剩余说明书(渲染器零改动)。waiting 步在此不可达——
476
+ // runStage 对任一 waiting 步的硬拦(上方 waiting 检查块 exit(1) 指引 --continue)先于本分支。
477
+ if (STAGE_BURST_STAGES.includes(stageName) && await readStageBurst(cwd)) {
478
+ return await renderStageBurst({ stageName, defSteps, cwd, changeName, platformOpts, progress, pm, specBase, scanProfile, stageData })
479
+ }
575
480
  // noAI 步骤自动完成(CLI-only,不需要 Agent 参与)
576
481
  if (defSteps[currentIdx].noAI || stageData.steps[currentIdx]?.noAI) {
577
482
  const stepName = defSteps[currentIdx].name
578
483
  const cliAction = defSteps[currentIdx]._cliAction || stageData.steps[currentIdx]?._cliAction
579
484
  console.log(`⚙️ Step ${currentIdx + 1}/${stageData.steps.length}: ${stepName}(CLI 自动执行,无需 Agent)`)
580
- if (cliAction === 'scanPreflight') {
581
- await executeScanPreflight(cwd, platformOpts, scanProfile)
582
- } else if (cliAction === 'scanPostcheck') {
583
- await executeScanPostcheck(cwd, platformOpts, scanProfile)
584
- } else if (cliAction === 'scanDetectProjects') {
585
- await executeScanDetectProjects(cwd, platformOpts)
586
- } else if (cliAction === 'scanResumeCheck') {
587
- await executeScanResumeCheck(cwd, platformOpts)
588
- } else if (cliAction === 'scanFinalize') {
589
- await executeScanFinalize(cwd, platformOpts)
590
- } else if (cliAction === 'planPostcheck') {
591
- await executePlanPostcheck(cwd, platformOpts, progress)
592
- } else if (cliAction === 'doctorRunDiagnostics') {
593
- // 2026-09-09-doctor-noai FR-01:doctor 阶段折叠——noAI 步跑全量诊断
594
- // (八维 + 三新 detector)+ renderDoctorSummary 渲染 + doctor-diagnosis.json 落盘
595
- const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('../doctor-diagnostics.js')
596
- const diag = await runDoctorDiagnostics({ cwd })
597
- console.log(renderDoctorSummary(diag))
598
- try { writeDoctorDiagnosis(diag, (platformOpts?.specRoot || join(cwd, '.sillyspec'))) } catch { /* 落盘 fail-soft */ }
599
- } else if (cliAction === 'progressConfirm') {
600
- // 主流程 step1「进度确认」noAI 化(2026-09-07):brainstorm/execute/verify——
601
- // 快照本就 CLI 注入、阶段路由已由 run 落定,省一轮「复述摘要→--done」往返
602
- executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
603
- } else if (cliAction === 'verifyRunQualityScan') {
604
- // P0-1(noai-ir-roadmap §3):verify「运行测试和质量扫描」noAI 化——test/lint 实测提前,
605
- // 指纹落盘供 --done 复用免重跑(长套件不再跑两遍);失败 throw 不盖 completed,
606
- // 输出 + 并行 WIP 归因提示透传给 agent(前置一/二)。
607
- const { executeVerifyQualityScan } = await import('./verify-quality-scan.js')
608
- await executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts })
609
- } else if (cliAction === 'archiveDistill') {
610
- // P0-4 安全变体(noai-ir-roadmap §3):archive「decision-distill 决策提炼」noAI 化——
611
- // 提炼本体是 CLI 纯函数(旧 prompt 即「调函数转述输出」纯中继);needsWait 裁决收敛到
612
- // 「确认归档 --confirm」。全路径不抛(裁决是确认步输入,非本步阻断条件)。
613
- const { executeArchiveDistill } = await import('./archive-distill.js')
614
- await executeArchiveDistill({ cwd, specBase, changeName })
615
- } else {
616
- throw new Error(`noAI 步骤 ${stepName} 的未知 _cliAction: ${cliAction}——请在 stage.js 注册对应分支`)
617
- }
485
+ await executeNoAiCliAction({ cliAction, stepName, stageName, cwd, specBase, changeName, platformOpts, progress, pm, scanProfile, stageData })
618
486
  stageData.steps[currentIdx].status = 'completed'
619
487
  stageData.steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
620
488
  pm._write(cwd, progress, changeName)
@@ -624,21 +492,8 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
624
492
  console.log('')
625
493
  await outputStep(stageName, nextIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, stageName))
626
494
  } else {
627
- // 所有步骤完成
628
- stageData.status = 'completed'
629
- stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
630
- // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08;与 complete.js 两处同语义)
631
- if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
632
- // persist _write 移到 completeStageGates 成功之后(task-02 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
633
- // 阶段完成收尾共享管线(noAI 末步核心修复 S1:plan postcheck independent-tier review verdict=fail /
634
- // 平台 scan manifest 此前被绕过)。gate 失败已 rollback 为 in-progress,early-return(不 fall through 到末尾 return)。
635
- const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText: null })
636
- // task-04 / A5:gate 失败(stageCompleted===false)设进程退出码 1(与 completeStep/continueStep 同语义)。
637
- if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
638
- if (_stageGatesResult) return _stageGatesResult
639
- // gate 全过:persist completed(task-02 移后;此处无 triggerSync,与同文件 noAI 末步语义一致)。
640
- pm._write(cwd, progress, changeName)
641
- console.log(`\n✅ ${stageName} 阶段全部完成。`)
495
+ // 所有步骤完成——收尾管线抽为 finalizeStageAllStepsDone(burst 全 noAI 场景共用,防第二副本)
496
+ return await finalizeStageAllStepsDone({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, currentIdx })
642
497
  }
643
498
  return
644
499
  }
@@ -646,6 +501,117 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
646
501
  }
647
502
  }
648
503
 
504
+ // ── burst 阶段折叠助手族(2026-09-22-stage-burst-fold,D-002/D-006/D-008)──
505
+ // 白名单 STAGE_BURST_STAGES 自 shared.js import(与 command.js --done 分发门共用单一事实源)。
506
+
507
+ /**
508
+ * noAI 步 _cliAction 分发(原 runStage noAI 分支 if-链原样抽取,D-008——常规单步路径与
509
+ * burst 渲染循环共同调用,单一事实源防第三副本;行为零变化:同一 if-链、同一入参语义。
510
+ * complete.js 的 --done 路径平行副本不动——"不改 completeStep 本体"边界)。
511
+ */
512
+ async function executeNoAiCliAction({ cliAction, stepName, stageName, cwd, specBase, changeName, platformOpts, progress, pm, scanProfile, stageData }) {
513
+ if (cliAction === 'scanPreflight') {
514
+ await executeScanPreflight(cwd, platformOpts, scanProfile)
515
+ } else if (cliAction === 'scanPostcheck') {
516
+ await executeScanPostcheck(cwd, platformOpts, scanProfile)
517
+ } else if (cliAction === 'scanDetectProjects') {
518
+ await executeScanDetectProjects(cwd, platformOpts)
519
+ } else if (cliAction === 'scanResumeCheck') {
520
+ await executeScanResumeCheck(cwd, platformOpts)
521
+ } else if (cliAction === 'scanFinalize') {
522
+ await executeScanFinalize(cwd, platformOpts)
523
+ } else if (cliAction === 'planPostcheck') {
524
+ await executePlanPostcheck(cwd, platformOpts, progress)
525
+ } else if (cliAction === 'doctorRunDiagnostics') {
526
+ // 2026-09-09-doctor-noai FR-01:doctor 阶段折叠——noAI 步跑全量诊断
527
+ // (八维 + 三新 detector)+ renderDoctorSummary 渲染 + doctor-diagnosis.json 落盘
528
+ const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('../doctor-diagnostics.js')
529
+ const diag = await runDoctorDiagnostics({ cwd })
530
+ console.log(renderDoctorSummary(diag))
531
+ try { writeDoctorDiagnosis(diag, (platformOpts?.specRoot || join(cwd, '.sillyspec'))) } catch { /* 落盘 fail-soft */ }
532
+ } else if (cliAction === 'progressConfirm') {
533
+ // 主流程 step1「进度确认」noAI 化(2026-09-07):brainstorm/execute/verify——
534
+ // 快照本就 CLI 注入、阶段路由已由 run 落定,省一轮「复述摘要→--done」往返
535
+ executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
536
+ } else if (cliAction === 'verifyRunQualityScan') {
537
+ // P0-1(noai-ir-roadmap §3):verify「运行测试和质量扫描」noAI 化——test/lint 实测提前,
538
+ // 指纹落盘供 --done 复用免重跑(长套件不再跑两遍);失败 throw 不盖 completed,
539
+ // 输出 + 并行 WIP 归因提示透传给 agent(前置一/二)。
540
+ const { executeVerifyQualityScan } = await import('./verify-quality-scan.js')
541
+ await executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts })
542
+ } else if (cliAction === 'archiveDistill') {
543
+ // P0-4 安全变体(noai-ir-roadmap §3):archive「decision-distill 决策提炼」noAI 化——
544
+ // 提炼本体是 CLI 纯函数(旧 prompt 即「调函数转述输出」纯中继);needsWait 裁决收敛到
545
+ // 「确认归档 --confirm」。全路径不抛(裁决是确认步输入,非本步阻断条件)。
546
+ const { executeArchiveDistill } = await import('./archive-distill.js')
547
+ await executeArchiveDistill({ cwd, specBase, changeName })
548
+ } else {
549
+ throw new Error(`noAI 步骤 ${stepName} 的未知 _cliAction: ${cliAction}——请在 stage.js 注册对应分支`)
550
+ }
551
+ }
552
+
553
+ /**
554
+ * 阶段全部步骤完成后的收尾管线(原 noAI 末步 :640-653 块原样抽取)——noAI 末步路径与
555
+ * burst 渲染全 noAI 场景共用。gate 失败 rollback + exitCode 语义逐字保留:
556
+ * persist _write 移到 completeStageGates 成功之后(gate 异常/失败 → rollback 回 in-progress
557
+ * 落盘,DB 不留假 completed);阶段完成收尾共享管线(plan postcheck independent-tier review
558
+ * verdict=fail / 平台 scan manifest 不被绕过)。
559
+ */
560
+ async function finalizeStageAllStepsDone({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, currentIdx }) {
561
+ stageData.status = 'completed'
562
+ stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
563
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08;与 complete.js 两处同语义)
564
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
565
+ const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null })
566
+ // task-04 / A5:gate 失败(stageCompleted===false)设进程退出码 1(与 completeStep/continueStep 同语义)。
567
+ if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
568
+ if (_stageGatesResult) return _stageGatesResult
569
+ // gate 全过:persist completed(此处无 triggerSync,与 noAI 末步语义一致)。
570
+ pm._write(cwd, progress, changeName)
571
+ console.log(`\n✅ ${stageName} 阶段全部完成。`)
572
+ return undefined
573
+ }
574
+
575
+ /**
576
+ * burst 折叠渲染(D-002,FR-02):单趟遍历 defSteps——跳过 completed/skipped;noAI 步就地
577
+ * CLI 执行 + 标完成落库(与单步 noAI 路径同语义);AI 步逐个调既有 outputStep(渲染器零改动,
578
+ * 首 AI 步的 persona/护栏注入由 outputStep 的 firstRenderableIdx 机制自然生效)。遍历后:
579
+ * 无剩余步 → finalizeStageAllStepsDone 收尾(全 noAI 场景);有剩余 → burst 尾提示。
580
+ * blocked/stale 步按「非 completed/skipped」正常进说明书(currentIdx 的 stale/blocked 已在
581
+ * runStage 上方拉回 pending;尾随 stale 步的完成侧拉回归 completeStepBurst 轮首,D-003@v2)。
582
+ */
583
+ async function renderStageBurst({ stageName, defSteps, cwd, changeName, platformOpts, progress, pm, specBase, scanProfile, stageData }) {
584
+ let lastNoAiIdx = -1
585
+ for (let i = 0; i < defSteps.length; i++) {
586
+ const st = stageData.steps[i]
587
+ if (st && (st.status === 'completed' || st.status === 'skipped')) continue
588
+ const isNoAI = defSteps[i]?.noAI || st?.noAI
589
+ if (isNoAI) {
590
+ const stepName = defSteps[i].name
591
+ const cliAction = defSteps[i]._cliAction || st?._cliAction
592
+ console.log(`⚙️ Step ${i + 1}/${stageData.steps.length}: ${stepName}(burst 就地 CLI 自动执行)`)
593
+ await executeNoAiCliAction({ cliAction, stepName, stageName, cwd, specBase, changeName, platformOpts, progress, pm, scanProfile, stageData })
594
+ if (st) {
595
+ st.status = 'completed'
596
+ st.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
597
+ }
598
+ pm._write(cwd, progress, changeName)
599
+ lastNoAiIdx = i
600
+ continue
601
+ }
602
+ await outputStep(stageName, i, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, stageName))
603
+ }
604
+ const remainingIdx = stageData.steps.findIndex(s => s && s.status !== 'completed' && s.status !== 'skipped')
605
+ if (remainingIdx === -1) {
606
+ // currentIdx=本轮最后执行的 noAI 步(与单步路径「刚执行步」同语义;尾随 skipped 场景不漂移——
607
+ // Grill execute 轮 P2-1),防御回退 defSteps.length - 1
608
+ return await finalizeStageAllStepsDone({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, currentIdx: lastNoAiIdx !== -1 ? lastNoAiIdx : defSteps.length - 1 })
609
+ }
610
+ console.log('')
611
+ console.log('📦 burst 模式:本阶段全部说明书已一次下发。干完全部步骤后用一次 --done 收口(CLI 内部逐步推进+逐步校验,失败即停在失败步)。')
612
+ return undefined
613
+ }
614
+
649
615
 
650
616
 
651
617