sillyspec 3.29.4 → 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 (148) 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-archive.md +26 -0
  10. package/assets/command-cards/run-brainstorm.md +33 -0
  11. package/assets/command-cards/run-execute.md +29 -0
  12. package/assets/command-cards/run-plan.md +27 -0
  13. package/assets/command-cards/run-quick.md +17 -0
  14. package/assets/command-cards/run-verify.md +29 -0
  15. package/assets/command-cards/status.md +27 -0
  16. package/package.json +2 -1
  17. package/packages/dashboard/server/executor.js +2 -1
  18. package/packages/dashboard/server/parser.js +7 -7
  19. package/r7-test-env/README-R7-TEST.md +93 -0
  20. package/r7-test-env/demo-project/lib/calc.js +6 -0
  21. package/r7-test-env/demo-project/package.json +1 -0
  22. package/r7-test-env/demo-project/test.js +10 -0
  23. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  24. package/r7-test-env/sillyspec-r7.cmd +3 -0
  25. package/round4/account.mjs +98 -0
  26. package/round4/b1-analyze-task08.mjs +40 -0
  27. package/round4/b1-b1-target-validation.md +32 -0
  28. package/round4/b3a-prompt-audit-result.md +66 -0
  29. package/round4/b3a-prompt-audit.mjs +89 -0
  30. package/round4/brief-autocompact.md +34 -0
  31. package/round4/brief-session-replay.md +50 -0
  32. package/round4/deep-read-synthesis.md +38 -0
  33. package/round4/extract-full.mjs +38 -0
  34. package/round4/extract-timeline.mjs +43 -0
  35. package/round4/find-sessions.mjs +13 -0
  36. package/round4/full-r4-l.md +4707 -0
  37. package/round4/full-r4-o-l.md +4300 -0
  38. package/round4/full-r4-os.md +1681 -0
  39. package/round4/full-r4-sf.md +4255 -0
  40. package/round4/full-r4-sq.md +1734 -0
  41. package/round4/gate-audit-relay-receipt.md +30 -0
  42. package/round4/gate-value-audit.md +52 -0
  43. package/round4/optimization-plan.md +165 -0
  44. package/round4/prereg.md +67 -0
  45. package/round4/prompt-R4-L.md +63 -0
  46. package/round4/prompt-R4-O-L.md +59 -0
  47. package/round4/prompt-R4-O-S.md +56 -0
  48. package/round4/prompt-R4-S-F.md +60 -0
  49. package/round4/prompt-R4-S-Q.md +66 -0
  50. package/round4/r4-final-report.html +347 -0
  51. package/round4/r4-os-timeline.txt +297 -0
  52. package/round4/r4-sf-timeline.txt +738 -0
  53. package/round4/r4-sq-timeline.txt +322 -0
  54. package/round4/r4l-timeline.txt +793 -0
  55. package/round4/r4ol-timeline.txt +729 -0
  56. package/round4/r4sf-timeline.txt +852 -0
  57. package/round4/r5-scenario-matrix.md +46 -0
  58. package/round4/result-S.md +171 -0
  59. package/round4/result-final.md +46 -0
  60. package/round4/result-quality.md +63 -0
  61. package/src/change-list.js +9 -0
  62. package/src/classify-change.js +6 -0
  63. package/src/command-cards.js +74 -0
  64. package/src/commit-guard.js +2 -0
  65. package/src/config-schema.js +64 -12
  66. package/src/contract-matrix.js +26 -5
  67. package/src/db.js +35 -4
  68. package/src/decision-distill.js +9 -3
  69. package/src/diagnostic-codes.js +2 -1
  70. package/src/docs-check.js +1 -1
  71. package/src/doctor-diagnostics.js +44 -2
  72. package/src/flow-draft.js +748 -0
  73. package/src/flow-parity.js +236 -0
  74. package/src/flow-review.js +184 -0
  75. package/src/flow.js +1492 -0
  76. package/src/foreign-declared.js +17 -1
  77. package/src/fr-index.js +396 -22
  78. package/src/fs-atomic.js +5 -1
  79. package/src/git-helper.js +2 -2
  80. package/src/greenfield-bootstrap.js +98 -0
  81. package/src/handoff.js +120 -7
  82. package/src/hunk-attribution.js +191 -0
  83. package/src/index.js +602 -21
  84. package/src/init.js +25 -8
  85. package/src/knowledge-classify.js +47 -1
  86. package/src/knowledge-digest.js +162 -0
  87. package/src/knowledge-quicklog.js +246 -0
  88. package/src/machine-draft.js +128 -0
  89. package/src/machine-interface.js +194 -7
  90. package/src/mcp-server.js +1 -0
  91. package/src/module-impact.js +51 -22
  92. package/src/module-resolve.js +3 -1
  93. package/src/preview-progress.js +160 -0
  94. package/src/progress/change-registry.js +11 -3
  95. package/src/progress/shared.js +3 -2
  96. package/src/progress.js +56 -13
  97. package/src/quicklog.js +44 -6
  98. package/src/redomain.js +147 -0
  99. package/src/review-material-pack.js +204 -1
  100. package/src/run/archive-distill.js +4 -2
  101. package/src/run/bg-sync.js +67 -0
  102. package/src/run/command.js +127 -10
  103. package/src/run/complete-handlers.js +363 -73
  104. package/src/run/complete.js +356 -74
  105. package/src/run/gate-snapshot-ledger.js +213 -0
  106. package/src/run/gate-snapshot.js +260 -38
  107. package/src/run/gates.js +482 -268
  108. package/src/run/green-cache.js +123 -0
  109. package/src/run/junction-rm.js +70 -0
  110. package/src/run/next.js +14 -0
  111. package/src/run/prompt.js +185 -55
  112. package/src/run/quick-audit.js +213 -14
  113. package/src/run/shared.js +123 -4
  114. package/src/run/stage.js +182 -216
  115. package/src/run/test-ledger.js +264 -0
  116. package/src/run/verify-quality-scan.js +84 -7
  117. package/src/scope-audit.js +6 -1
  118. package/src/sentinel-assertions.js +114 -0
  119. package/src/setup.js +3 -3
  120. package/src/spec-sync.js +41 -1
  121. package/src/stage-contract.js +26 -8
  122. package/src/stage-review-checklist.js +2 -0
  123. package/src/stage-review.js +7 -2
  124. package/src/stage-templates.js +157 -0
  125. package/src/stages/archive.js +2 -0
  126. package/src/stages/brainstorm.js +24 -106
  127. package/src/stages/execute.js +222 -85
  128. package/src/stages/knowledge.js +74 -1
  129. package/src/stages/plan-postcheck.js +385 -20
  130. package/src/stages/plan.js +23 -1
  131. package/src/stages/quick.js +10 -2
  132. package/src/stages/verify.js +4 -2
  133. package/src/sync.js +81 -23
  134. package/src/task-done.js +181 -0
  135. package/src/task-review.js +8 -55
  136. package/src/taskcard-frontmatter.js +57 -0
  137. package/src/test-bindings.js +549 -0
  138. package/src/timeline.js +218 -0
  139. package/src/ui-visual.js +175 -0
  140. package/src/verify-draft.js +214 -0
  141. package/src/verify-postcheck.js +919 -159
  142. package/src/verify-probes.js +103 -25
  143. package/src/watcher.js +1031 -0
  144. package/src/worktree-apply.js +44 -4
  145. package/src/worktree-deps.js +22 -8
  146. package/src/worktree-policy.js +42 -0
  147. package/src/worktree.js +102 -15
  148. package/templates/agents-instruction.md +37 -18
@@ -23,7 +23,7 @@
23
23
  * 是 prompt.js tier 注入链的组包入口——机械抽素材半边(designDigest/fileList/硬约束/diff/热区/
24
24
  * checklist),主代理点名半边(五交叉点/plan 差量)留位不预填(decisions.md D-002)。
25
25
  */
26
- import { readFileSync } from 'fs'
26
+ import { mkdirSync, readFileSync, writeFileSync } from 'fs'
27
27
  import { join } from 'path'
28
28
  import { REVIEW_CHECKLISTS } from './stage-review-checklist.js'
29
29
 
@@ -132,9 +132,15 @@ export function buildReviewMaterialPack(stage, inputs = {}) {
132
132
  const delta = (inputs.planDelta || []).map(d => `- [${d.id}] ${d.status}${d.note ? '——' + d.note : ''}`).join('\n')
133
133
  body = ['### design 硬约束', hc || '(无)', '### plan 差量(逐约束判定:一致/偏离/未覆盖)', delta || '(无——主代理未提供差量,评审者按 cannot_verify 列缺件)'].join('\n\n')
134
134
  } else if (stage === 'execute-qa') {
135
+ // 测试清单(R16 减负批次 2026-09-24):diff 命中的测试文件点名——QA 对「组装行为」条目
136
+ // 作答时不必全仓找测试(R14 对撞失控勘察子代理 2.33M 的反面教材后,包内点名收窄查证面)。
137
+ const testFiles = (inputs.diffSummary && inputs.diffSummary.files || [])
138
+ .filter(f => /(^|[\\/])(tests?|__tests__)([\\/])|\.(test|spec)\.[cm]?[jt]sx?$|_test\.(go|py)$|(^|[\\/])test_[^\\/]+\.py$/i.test(f))
139
+ .slice(0, 30)
135
140
  body = [
136
141
  '### diff 摘要', (inputs.diffSummary && inputs.diffSummary.files || []).map(f => '- ' + f).join('\n') || '(无)',
137
142
  inputs.diffSummary && inputs.diffSummary.stat ? '```diff\n' + inputs.diffSummary.stat + '\n```' : '',
143
+ '### 测试清单(diff 命中的测试文件)', testFiles.map(f => '- ' + f).join('\n') || '(diff 无测试文件——「组装行为」条目按包不足处理:cannot_verify 列缺件或定向查证并列明)',
138
144
  '### design 热区(CLI 抽取)', extractDesignHotZone(inputs.designContent || '') || '(无命中节)',
139
145
  '### 验收清单', (inputs.checklist || []).map(c => '- [ ] ' + c).join('\n') || '(无)',
140
146
  ].filter(Boolean).join('\n\n')
@@ -263,3 +269,200 @@ export async function assembleStageReviewMaterials({ stage, cwd, changeName, spe
263
269
  } catch { /* 装配 best-effort:失败零注入(与占位符缺失同态) */ }
264
270
  return ''
265
271
  }
272
+
273
+ // ══ execute 任务材料包(2026-09-21-r5-efficiency-batch1 task-02,B-④ / D-003)════
274
+ // 两段式装配:稳定段(design.md「接口定义」「文件变更清单」固定节名机械选取原文 + 签名锚点)
275
+ // 在前、专属段(task 卡 title/goal/implementation/acceptance + allowed_paths)在后——稳定前缀
276
+ // 跨任务共享,兑现统一前缀缓存收益(压 task-08 型子代理轮均 148K vs 同侪 45-92K 差额)。
277
+ // 只摘不译(D-003 错键正典红线):一切内容为源文件原文逐字摘录/字段机械抽取,禁转写缩写。
278
+
279
+ /** 材料包字节上限(专属段尾部优先截;稳定段超限按节优先级截断——见 assembleExecuteTaskMaterials) */
280
+ const EXEC_MATERIALS_MAX_BYTES = 24576
281
+ /** 被截节保底形态里「首个代码块/表格」的块级上限(单块病态体量防穿顶) */
282
+ const EXEC_STUB_BLOCK_MAX_BYTES = 3600
283
+
284
+ function bytesOf(s) { return Buffer.byteLength(String(s || ''), 'utf8') }
285
+
286
+ /** 字节安全截断(按 UTF-8 字节预算切,不切断多字节字符) */
287
+ function byteSafeSlice(text, maxBytes) {
288
+ const s = String(text || '')
289
+ if (bytesOf(s) <= maxBytes) return s
290
+ let out = ''
291
+ let used = 0
292
+ for (const ch of s) {
293
+ const b = Buffer.byteLength(ch, 'utf8')
294
+ if (used + b > maxBytes) break
295
+ out += ch
296
+ used += b
297
+ }
298
+ return out
299
+ }
300
+
301
+ /** 节内首个完整代码块(``` 围栏)或表格(连续 | 行)——被截节的保底保留形态 */
302
+ function firstBlockOrTable(body) {
303
+ const fence = body.match(/```[^\n]*\n[\s\S]*?```/)
304
+ if (fence) return fence[0]
305
+ const lines = body.split('\n')
306
+ const start = lines.findIndex(l => l.trim().startsWith('|'))
307
+ if (start >= 0) {
308
+ let end = start
309
+ while (end < lines.length && lines[end].trim().startsWith('|')) end++
310
+ return lines.slice(start, end).join('\n')
311
+ }
312
+ return null
313
+ }
314
+
315
+ /** task 卡 frontmatter 块级解析(机械逐字):顶格 key 行归属,其后缩进行归当前 key */
316
+ function splitTaskCardFields(cardText) {
317
+ const m = String(cardText || '').match(/^---\r?\n([\s\S]*?)\r?\n---/)
318
+ if (!m) return {}
319
+ const fields = {}
320
+ let cur = null
321
+ for (const line of m[1].split(/\r?\n/)) {
322
+ if (/^[A-Za-z_][\w-]*:/.test(line)) {
323
+ const at = line.indexOf(':')
324
+ cur = { key: line.slice(0, at), raw: line.slice(at + 1).trim(), lines: [] }
325
+ fields[cur.key] = cur
326
+ } else if (cur) {
327
+ cur.lines.push(line)
328
+ }
329
+ }
330
+ return fields
331
+ }
332
+
333
+ function unquoteScalar(v) {
334
+ const s = String(v || '')
335
+ return s.replace(/^'(.*)'$/s, '$1').replace(/^"(.*)"$/s, '$1')
336
+ }
337
+
338
+ /**
339
+ * task 卡字段取值(只摘不译——原文保持,无 YAML 折叠语义改写):
340
+ * 块标量(>/|)→ 原始行去公共缩进后逐行拼接;列表 → `- ` 后原文数组;标量 → 去包裹引号。
341
+ * @returns {string|string[]}
342
+ */
343
+ function taskCardFieldValue(field) {
344
+ if (!field) return ''
345
+ if (/^[>|][+-]?$/.test(field.raw)) {
346
+ const indents = field.lines.filter(l => l.trim()).map(l => l.match(/^[ \t]*/)[0].length)
347
+ const cut = indents.length ? Math.min(...indents) : 0
348
+ return field.lines.map(l => (l.trim() ? l.slice(cut) : '')).join('\n').trim()
349
+ }
350
+ if (field.raw) return unquoteScalar(field.raw)
351
+ const items = []
352
+ for (const l of field.lines) {
353
+ const im = l.match(/^\s*-\s?(.*)$/)
354
+ if (im && im[1].trim()) items.push(im[1].trim())
355
+ }
356
+ return items
357
+ }
358
+
359
+ /**
360
+ * 组装 execute 任务材料包(两段式,只摘不译——契约见 change design.md「接口定义」)。
361
+ * @param {object} opts
362
+ * @param {string} opts.changeDir - 变更目录(读 design.md / tasks/task-NN.md)
363
+ * @param {string} opts.taskId - 'task-NN'
364
+ * @param {string} [opts.materialsDir] - 落盘目录;缺省 → 不落盘不抛错返回 null(additive)
365
+ * @param {Array<{symbol: string, file: string, line: number}>} [opts.signatureAnchors]
366
+ * - 接口签名锚点(可选,调用方供给;缺省空)
367
+ * @returns {Promise<{path: string, bytes: number, truncated: boolean}|null>}
368
+ * 截尾序:①专属段尾部优先截(稳定段全量保留);②稳定段自身超限按节优先级(接口定义 >
369
+ * 文件变更清单——高优先节后截)逐节降为「节头 + 首个代码块/表格 + 回源指引行」;锚点永不丢。
370
+ */
371
+ export async function assembleExecuteTaskMaterials({ changeDir, taskId, materialsDir, signatureAnchors }) {
372
+ if (!changeDir || !taskId || !materialsDir) return null
373
+ const designPath = join(changeDir, 'design.md')
374
+ const taskCardPath = join(changeDir, 'tasks', `${taskId}.md`)
375
+ let designContent = ''
376
+ let cardContent = ''
377
+ try { designContent = readFileSync(designPath, 'utf8') } catch { /* 缺 design → 稳定段只剩锚点 */ }
378
+ try { cardContent = readFileSync(taskCardPath, 'utf8') } catch { /* 缺卡 → 专属段仅回源指引 */ }
379
+
380
+ // ── 稳定段素材:固定节名机械选取(缺节跳过)+ 签名锚点行 ──
381
+ const anchors = (Array.isArray(signatureAnchors) ? signatureAnchors : [])
382
+ .filter(a => a && a.symbol)
383
+ .map(a => `- \`${String(a.symbol)}\` → ${a.file || '?'}:${a.line || '?'}`)
384
+ const anchorsBlock = anchors.length ? `### 接口签名锚点\n\n${anchors.join('\n')}` : ''
385
+
386
+ const SECTION_SPECS = [{ name: '接口定义' }, { name: '文件变更清单' }] // 序 = 保留优先级序
387
+ const secBlocks = SECTION_SPECS
388
+ .map(s => ({ ...s, body: sectionBody(designContent, s.name) }))
389
+ .filter(s => s.body)
390
+ .map(s => ({ ...s, full: `### ${s.name}(design.md 原文摘录)\n\n${s.body}` }))
391
+
392
+ /** 被截节残体:节头 + 首个代码块/表格(块级 clamp)+ 一行回源指引 */
393
+ const stubOf = s => {
394
+ const first = firstBlockOrTable(s.body)
395
+ const kept = first
396
+ ? (bytesOf(first) > EXEC_STUB_BLOCK_MAX_BYTES ? byteSafeSlice(first, EXEC_STUB_BLOCK_MAX_BYTES) + '\n…(首块超限截断)' : first)
397
+ : byteSafeSlice(s.body, 600)
398
+ return `### ${s.name}(design.md 原文摘录)\n\n${kept}\n\n完整内容回源:${designPath}#${s.name}`
399
+ }
400
+
401
+ // ── 专属段素材:task 卡要点(机械摘录)+ allowed_paths + 回源指引 ──
402
+ const fields = splitTaskCardFields(cardContent)
403
+ const val = k => taskCardFieldValue(fields[k])
404
+ const bulletList = v => (Array.isArray(v) ? v.map(i => ` - ${i}`).join('\n') : ` - ${v}`)
405
+ const title = val('title')
406
+ const goal = val('goal')
407
+ const impl = val('implementation')
408
+ const acc = val('acceptance')
409
+ const paths = val('allowed_paths')
410
+ const cardLines = [
411
+ title ? `- title:${title}` : '',
412
+ goal ? `- goal:${goal}` : '',
413
+ impl ? `- implementation:\n${bulletList(impl)}` : '',
414
+ acc ? `- acceptance:\n${bulletList(acc)}` : '',
415
+ ].filter(Boolean).join('\n')
416
+ const dedicatedFull = [
417
+ cardLines ? `### 任务卡要点(tasks/${taskId}.md 摘录)\n\n${cardLines}` : '',
418
+ Array.isArray(paths) && paths.length ? `### allowed_paths\n\n${paths.map(p => `- ${p}`).join('\n')}` : '',
419
+ `### 回源指引\n\n- design.md:${designPath}\n- 任务卡:${taskCardPath}`,
420
+ ].filter(Boolean).join('\n\n')
421
+
422
+ // ── 装配(稳定段在前、专属段在后)+ 字节预算 ──
423
+ const packTitle = `# execute 任务材料包:${taskId}`
424
+ const packNote = '> 两段式:稳定段(design 契约节原文摘录)在前、专属段(task 卡摘录)在后。\n> 只摘不译——正文均为源文件原文逐字摘录;与源文件冲突时以源文件为准,按锚点回源核对。'
425
+ const stableTitle = '## 稳定段(design 契约节原文摘录——跨任务稳定)'
426
+ const dedicatedTitle = `## 专属段(${taskId} 专属)`
427
+ const tailMarker = `…(专属段超限截尾——完整内容回源:${taskCardPath})`
428
+ const joinParts = parts => parts.filter(Boolean).join('\n\n')
429
+ const assemble = (secs, dedicatedText) =>
430
+ joinParts([packTitle, packNote, stableTitle, ...secs, anchorsBlock, dedicatedTitle, dedicatedText])
431
+
432
+ let truncated = false
433
+ const sections = secBlocks.map(s => s.full)
434
+ let content = assemble(sections, dedicatedFull)
435
+
436
+ if (bytesOf(content) > EXEC_MATERIALS_MAX_BYTES) {
437
+ truncated = true
438
+ // ① 专属段尾部优先截:稳定段全量保留,专属段按剩余预算截尾
439
+ const clampDedicated = () => {
440
+ const head = assemble(sections, '')
441
+ const room = EXEC_MATERIALS_MAX_BYTES - bytesOf(head) - bytesOf('\n\n') - bytesOf(tailMarker) - 1
442
+ if (room < 0) return false
443
+ const sliced = byteSafeSlice(dedicatedFull, room)
444
+ content = head + '\n\n' + (sliced ? sliced + '\n' + tailMarker : tailMarker)
445
+ return true
446
+ }
447
+ if (!clampDedicated()) {
448
+ // ② 稳定段自身超限:按节优先级(接口定义 > 文件变更清单)低优先节先降为残节,逐节重试
449
+ for (let i = secBlocks.length - 1; i >= 0; i--) {
450
+ sections[i] = stubOf(secBlocks[i])
451
+ if (clampDedicated()) break
452
+ }
453
+ }
454
+ // ③ 终极兜底(双节皆残仍超限——仅病态锚点面可达):字节安全截 + 锚点块强制保留
455
+ if (bytesOf(content) > EXEC_MATERIALS_MAX_BYTES && anchorsBlock) {
456
+ const cut = content.indexOf(anchorsBlock)
457
+ const before = (cut > 0 ? content.slice(0, cut) : content).replace(/\n+$/, '')
458
+ content = byteSafeSlice(before, EXEC_MATERIALS_MAX_BYTES - bytesOf('\n\n' + anchorsBlock)) + '\n\n' + anchorsBlock
459
+ } else if (bytesOf(content) > EXEC_MATERIALS_MAX_BYTES) {
460
+ content = byteSafeSlice(content, EXEC_MATERIALS_MAX_BYTES)
461
+ }
462
+ }
463
+
464
+ mkdirSync(materialsDir, { recursive: true })
465
+ const outPath = join(materialsDir, `${taskId}.md`)
466
+ writeFileSync(outPath, content, 'utf8')
467
+ return { path: outPath, bytes: bytesOf(content), truncated }
468
+ }
@@ -48,8 +48,10 @@ export async function executeArchiveDistill({ cwd, specBase, changeName }) {
48
48
 
49
49
  // ── FR 索引提炼(2026-09-18-fr-index-l1 L1:稳定 id 发号+承接翻链;同 best-effort 降级语义)──
50
50
  try {
51
- const { indexRequirements } = await import('../fr-index.js')
52
- const fr = indexRequirements({ changeDir, knowledgeRoot, headHash })
51
+ const { indexRequirements, archiveDeliverableFiles } = await import('../fr-index.js')
52
+ // greenfield-bootstrap:供 deliverableFiles(design 表 ∪ apply-manifest)——此前缺供致域路由
53
+ // 退化为 design 清单单源,全泛化段时落 unmapped(R17 臂3 实证),与轻量道口径对齐。
54
+ const fr = indexRequirements({ changeDir, knowledgeRoot, headHash, deliverableFiles: archiveDeliverableFiles(changeDir) })
53
55
  const frWritten = Array.isArray(fr && fr.written) ? fr.written : []
54
56
  if (fr && fr.skipped) {
55
57
  console.log(`\nℹ️ FR 索引零输出:${fr.skipped}`)
@@ -52,6 +52,11 @@ const LOCK_STALE_MS = 15 * 60 * 1000;
52
52
  const LOG_TRUNCATE_THRESHOLD = 1_000_000;
53
53
  const LOG_TRUNCATE_KEEP = 512 * 1024;
54
54
 
55
+ // 终态补推扫描窗口与上限(2026-09-23 quick ql-20260923-007):候选取 last_active 倒序
56
+ // 前 50 行(老终态缺位也收敛,但不全表扫描);一轮最多补 3 条(防多缺位风暴挤爆预算)。
57
+ const TERMINAL_SWEEP_CANDIDATES = 50;
58
+ export const TERMINAL_SWEEP_MAX = 3;
59
+
55
60
  /** pid 活性探测(跨平台):signal 0 探测;EPERM = 进程存在但属他户,保守按活。 */
56
61
  function isPidAlive(pid) {
57
62
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -178,6 +183,48 @@ export async function spawnBackgroundSync(cwd, changeName, platformOpts = {}, op
178
183
  return { status: 'spawned', logPath };
179
184
  }
180
185
 
186
+ /**
187
+ * 归档终态补推扫描:DB 里 status=archived/deleted 且本地终态脏于平台镜像
188
+ * (last_local_modified_ts > last_synced_platform_ts,或从未同步)的变更名,last_active
189
+ * 倒序取前 TERMINAL_SWEEP_MAX 条。
190
+ *
191
+ * 背景(2026-09-22 session-fork-continuation 实证):终态推送依赖归档后还有一轮
192
+ * triggerSync——bg 子进程 best-effort,spawn 失败/单飞锁竞态/会话在归档后戛然而止都会
193
+ * 吞掉最后一轮,本地已归档而平台镜像停在旧阶段(verify),此后再无命令碰该变更即永久
194
+ * 滞后。归档/删除收尾均 _touchLocalModified(change-registry 语义:本地状态推进),
195
+ * 谓词即「终态戳晚于最后一次成功推送」。补推幂等(sync() 对 archived/deleted 推终态+
196
+ * 墓碑,POST 幂等),任意后续命令触发的 bg 轮顺带收敛。
197
+ * 时钟口径:last_synced_platform_ts 是服务端时间,本地钟超前服务端时至多多补一轮幂等
198
+ * 推送(无副作用),且 synced 随每次成功推进,自愈。
199
+ * 库路径与 sync() 内 ProgressManager 同源(resolvePlatformSpecDir(cwd):平台指针→向上
200
+ * 发现→cwd/.sillyspec 默认;platformOpts.specRoot 不进库路径——与 triggerSync 对齐),
201
+ * 保证扫的库就是主轮推的库;解析抛错(指针失效等)→ 空集跳过本轮(best-effort)。
202
+ */
203
+ export async function collectTerminalSyncPending(cwd) {
204
+ const { resolvePlatformSpecDir, ProgressManager } = await import('../progress.js');
205
+ let specDir = null;
206
+ try { specDir = resolvePlatformSpecDir(cwd); } catch { return []; }
207
+ if (!specDir) return [];
208
+ if (!existsSync(join(specDir, '.runtime', 'sillyspec.db'))) return [];
209
+ const pm = new ProgressManager({ specDir });
210
+ const rows = pm._ensureDB(cwd).getDb().prepare(
211
+ `SELECT name, last_active, last_synced_platform_ts, last_local_modified_ts
212
+ FROM changes WHERE status IN ('archived','deleted')
213
+ ORDER BY last_active DESC LIMIT ${TERMINAL_SWEEP_CANDIDATES}`
214
+ ).all();
215
+ const pending = [];
216
+ for (const r of rows) {
217
+ if (!r.name) continue;
218
+ const synced = r.last_synced_platform_ts || '';
219
+ const local = r.last_local_modified_ts || '';
220
+ // 脏度戳必须在场(fail-closed):D-013 之前的陈年行两戳皆空,无从判滞后——补推只会
221
+ // 给平台凭空造新行;synced 空而 local 在场 = 从未成功推送过但终态有据 → 补。
222
+ if (local && (!synced || local > synced)) pending.push(r.name);
223
+ if (pending.length >= TERMINAL_SWEEP_MAX) break;
224
+ }
225
+ return pending;
226
+ }
227
+
181
228
  /**
182
229
  * 子侧入口(bootstrap 经 env 传参调起;env 注入默认值供测试覆盖)。
183
230
  *
@@ -224,6 +271,26 @@ export async function runBgSyncFromEnv(env = process.env) {
224
271
  break;
225
272
  }
226
273
  }
274
+ // 终态补推(2026-09-23 quick ql-20260923-007):主轮收尾后扫 DB,补推「本地已达终态但
275
+ // 平台镜像滞后」的变更(session-fork 实证缺口)。幂等;主变更刚推过跳过;预算耗尽留下一轮。
276
+ try {
277
+ const pending = await collectTerminalSyncPending(cwd);
278
+ for (const name of pending) {
279
+ if (name === changeName) continue;
280
+ if (Date.now() + roundTimeoutMs > deadline) {
281
+ console.warn('[spec-sync-bg] 总预算用尽,终态补推剩余条目由下一轮 bg 接管');
282
+ break;
283
+ }
284
+ console.log(`[spec-sync-bg] 终态补推(本地终态未被平台镜像): ${name}`);
285
+ try {
286
+ await triggerSync(cwd, name, platformOpts, { inline: true, timeoutMs: roundTimeoutMs });
287
+ } catch (e) {
288
+ console.warn(`[spec-sync-bg] 终态补推异常(best-effort,下轮重试): ${(e && e.message) || e}`);
289
+ }
290
+ }
291
+ } catch (e) {
292
+ console.warn(`[spec-sync-bg] 终态补推扫描异常(best-effort): ${(e && e.message) || e}`);
293
+ }
227
294
  console.log(`[spec-sync-bg] 后台同步完成 ${new Date().toISOString()}: ${changeName}`);
228
295
  removeBgSyncLock(runtimeRoot);
229
296
  }
@@ -20,14 +20,14 @@
20
20
  * './run/stage.js' → './shared.js' 等同级
21
21
  */
22
22
  import { basename, join, resolve, dirname } from 'node:path'
23
- import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync } from 'node:fs'
23
+ import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync, statSync } from 'node:fs'
24
24
  import { randomBytes, randomUUID } from 'node:crypto'
25
25
  import { fileURLToPath } from 'node:url'
26
26
  import { writeAtomicSync } from '../fs-atomic.js'
27
- import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce } from './shared.js'
27
+ import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce, readStageBurst, STAGE_BURST_STAGES } from './shared.js'
28
28
  import { resolveQuickLinkedChanges } from './quick-audit.js'
29
29
  import { outputStep, collectStageWaitHistory } from './prompt.js'
30
- import { completeStep, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
30
+ import { completeStep, completeStepBurst, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
31
31
  import { runStage } from './stage.js'
32
32
  import { sanitizeDesc } from '../quicklog.js'
33
33
  import { ProgressManager, resolveSessionIdentity } from '../progress.js'
@@ -52,6 +52,7 @@ const VALUE_FLAGS = new Set([
52
52
  '--base', // scan diff 基线 commit(吃值;只在 run scan --diff 转发路径消费)
53
53
  '--session', // 显式会话标识(吃值;2026-09-14-change-ownership-guards task-02 所有权三级解析最高优先级层,启动 claim 消费)
54
54
  '--inherit-from', // wait 继承盖章锚点 <D-xxx@vN>(吃值;2026-09-18-preflight-slimming task-03 / D-003@v2:仅 --wait 场景合法,--wait 分发点校验消费)
55
+ '--step', // --done --step <名|序号> 意图断言(吃值;:340 消费。漏登记死路同 --wait-interactive 前例:burst 说明书出示该形态却被未知参数 exit(2) 拦死,2026-09-23 执行会话实证)
55
56
  ])
56
57
 
57
58
  /**
@@ -511,6 +512,10 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
511
512
  console.error(`❌ 检测到祖先链有 ${ancestorSpecs.length} 个 .sillyspec 实例(monorepo 多实例),当前使用: ${specBase}`)
512
513
  console.error(` 当前 cwd 不在 git 顶层仓库根下,静默绑定到子项目实例会导致进度/QUICKLOG 分裂。`)
513
514
  console.error(` 修复:cd 回项目根目录,或用 --spec-dir <根>/.sillyspec 显式指定。`)
515
+ // 一键指定整行(R4-S-Q 实证:嵌套 worktree 场景下 agent 多次试错才拼对放行组合)——
516
+ // 意图实例即当前最近命中(specBase),直接给出可贴完整命令行。
517
+ console.error(` 一键指定(当前最近实例,整行照贴):`)
518
+ console.error(` sillyspec ${process.argv.slice(2).join(' ')} --spec-dir "${specBase}"`)
514
519
  process.exit(2)
515
520
  }
516
521
  // 在 git 根下但有多实例(子项目也有 .sillyspec)→ 仅警告(正确实例已命中)
@@ -716,6 +721,10 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
716
721
  '--takeover', // 所有权护栏显式强制接管(task-02 / FR-01:本层只注册透传,实际消费在 index.js apply/cleanup 子命令层——run 侧带上不报未知参数,语义出口在 worktree 命令)
717
722
  '--skip-apply', // 归档收口跳过 apply 校验(task-02 只注册透传不改行为,消费归 task-03 archive 接线)
718
723
  '--inherit-from', // wait 继承盖章 <D-xxx@vN>(2026-09-18-preflight-slimming task-03:仅 --wait 场景合法,hasDecisionId 校验+盖章在 --wait 分发点/complete 层)
724
+ '--step', // --done 意图断言 <名|序号>(吃值,VALUE_FLAGS 同步登记;:340 消费——漏登记则说明书出示的 --done --step 形态进命令即被未知参数拦死,2026-09-23 执行会话实证)
725
+ '--upgrade-thick', // 轻量→完整升档用户同意门(2026-09-25-thin-upgrade-consent:布尔 flag,混跑回退写侧 :1362 消费——无 flag 拒跑,带 flag 落 legacy_fallback+同意时点留痕)
726
+ '--same-session', // R17 实验实证死路清偿(cli-protocol-trust):STAGE_WALL 报错指引「加 --same-session 重跑」(stage.js 硬墙文案),消费在 runStage 逃生口——此前漏登记,照指引重跑即被本白名单 exit 2(指引指向死路的自我矛盾)
727
+ '--force', // 同族第二实例(静态扫描新发现):quick --cancel 强清消费点与报错指引(可用 --force)在场但未登记——同死路;一致性钉 test/flag-contract.test.mjs 守护整类
719
728
  '-h',
720
729
  ])
721
730
  for (let i = 0; i < flags.length; i++) {
@@ -791,6 +800,43 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
791
800
  } catch {}
792
801
  }
793
802
  if (!quickSessionId) {
803
+ // 坑 quick-done-长静默与快照行尾假阳性 坑3(2026-09-21 实证):`quick --status` 不带
804
+ // --change 时缺省语义是「新建」而非「查看」——查进度忘带 --change 直接误开空壳会话
805
+ //(需 --cancel 清理)。只读意图(--status)拦截:读 current marker 与最近 guard 目录
806
+ // 展示最近会话状态 + 提示带 --change,零副作用退出(绝不新建)。
807
+ if (isStatus) {
808
+ const recentRoot = resolveRuntimeRoot(platformOpts, specRoot)
809
+ let recent = null
810
+ try {
811
+ const idFile = join(recentRoot, 'current-quick-run-id')
812
+ if (existsSync(idFile)) {
813
+ const v = readFileSync(idFile, 'utf8').trim()
814
+ if (QUICK_SID_RE.test(v)) recent = v
815
+ }
816
+ } catch { /* 读失败按无 marker 处理 */ }
817
+ if (!recent) {
818
+ try {
819
+ const sessRoot = join(recentRoot, 'quick-sessions')
820
+ if (existsSync(sessRoot)) {
821
+ const cands = readdirSync(sessRoot, { withFileTypes: true })
822
+ .filter(d => d.isDirectory() && QUICK_SID_RE.test(d.name))
823
+ .map(d => ({ name: d.name, m: statSync(join(sessRoot, d.name, 'guard.json')).mtimeMs }))
824
+ .filter(x => x.m > 0)
825
+ .sort((a, b) => b.m - a.m)
826
+ if (cands.length > 0) recent = cands[0].name
827
+ }
828
+ } catch { /* 枚举失败按无近期会话处理 */ }
829
+ }
830
+ console.log('ℹ️ `run quick --status` 不带 --change 查的是最近会话,不会新建(查进度请带 --change <sessionId>)')
831
+ if (recent) {
832
+ console.log(` 最近会话: ${recent}(本命令将展示其状态)`)
833
+ quickSessionId = recent
834
+ quickFallbackUsed = true
835
+ } else {
836
+ console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更:sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"')
837
+ process.exit(0)
838
+ }
839
+ } else {
794
840
  // 坑 quick-no-input-placeholder-title(2026-08-24 二期①;2026-09-19 上移):全新 quick 会话
795
841
  // (即将生成新 sessionId)缺 --input 且无可取标题的 --linked-changes → 拒绝启动(exit 2,
796
842
  // 零沉没成本)。原位置在 flag 校验之后,但 sessionId 生成/「已建立」公告与 agent-log 平台
@@ -824,6 +870,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
824
870
  // 若把命令拖过 exec 超时,调用方仍能从输出头部拿到 sessionId 续用(--status / guard.json
825
871
  // 核对),不会误开空壳新会话。
826
872
  console.log(`📌 quick 会话已建立: ${quickSessionId}(后续若超时/中断,用 --change ${quickSessionId} 续用)`)
873
+ }
827
874
  }
828
875
  changeName = quickSessionId
829
876
  } else {
@@ -1301,6 +1348,45 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1301
1348
  // 注册变更到全局活跃列表(如果尚未注册)
1302
1349
  if (effectiveChange) {
1303
1350
  pm.registerChange(cwd, effectiveChange)
1351
+ // ── watcher 拉起(R7 切片一 / D-001:观测与协议解耦)──
1352
+ // effectiveChange 解析后无条件 spawn(不做「检测新建」——registerChange 是 INSERT OR
1353
+ // IGNORE 不返回新建信号),活锁在跑由单飞锁合并 coalesced。best-effort:失败只 warn
1354
+ // 绝不阻断主流程;未连接平台也 spawn(本地 jsonl 是事件唯一真相源);
1355
+ // SILLYSPEC_WATCHER=0 逃生阀。default 容器行不监听(非真实变更)。
1356
+ if (effectiveChange !== 'default') {
1357
+ // ── 混跑回退写侧(R7 切片二 / FR-06 / D-002:thin change 上跑 run <stage> = 该 change
1358
+ // 回 legacy 记账——flow done 读侧拒裁并指路厚档;两套记账不叠加。一次性置位留痕。)──
1359
+ // 升厚同意门(2026-09-25-thin-upgrade-consent,R16 实测驱动):轻量→完整是成本数倍的资源
1360
+ // 决策,归属用户——agent 不得凭 CLI 的 advisory 自行转道。混跑回退须显式 --upgrade-thick
1361
+ // (=「用户已同意」的落痕 flag,同 --force 留痕先例);无 flag 拒跑指路。
1362
+ try {
1363
+ const flowStatePath = join(specRoot, 'changes', effectiveChange, 'flow-state.yaml')
1364
+ if (existsSync(flowStatePath)) {
1365
+ const { readFlowState, writeFlowState } = await import('../flow.js')
1366
+ const changeDir = join(specRoot, 'changes', effectiveChange)
1367
+ const st = readFlowState(changeDir)
1368
+ if (st && !st.legacy_fallback) {
1369
+ if (!flags.includes('--upgrade-thick')) {
1370
+ console.error(`⛔ 本变更在轻量跑道上(flow-state 在场)——升厚走完整流程需用户同意,agent 不得自行转道:`)
1371
+ console.error(` · 征得用户同意后带 --upgrade-thick 重跑本命令(落痕留档,首次转厚即转)`)
1372
+ console.error(` · 用户未确认或选择轻量跑 → 继续轻量变更:直接干活,收口 sillyspec flow done --change ${effectiveChange}`)
1373
+ process.exit(2)
1374
+ }
1375
+ writeFlowState(changeDir, { legacy_fallback: true, upgraded_by_consent: new Date().toISOString() })
1376
+ console.warn(`⚠️ [flow] thin change「${effectiveChange}」经用户同意(--upgrade-thick 落痕)升厚——已混跑回退 legacy(flow done 将按厚档拒裁并指路;两套记账不叠加)`)
1377
+ }
1378
+ }
1379
+ } catch { /* 探测失败不阻断既有 run 流程(process.exit 直出不经 catch) */ }
1380
+ try {
1381
+ const { spawnWatcher } = await import('../watcher.js')
1382
+ const r = await spawnWatcher(cwd, effectiveChange, { ...platformOpts, specBase })
1383
+ if (r.status === 'spawned') {
1384
+ console.log(`🔄 [watcher] 观测旁路已拉起(产物签名轮询,事件恒带 provisional:true): ${effectiveChange}`)
1385
+ }
1386
+ } catch (e) {
1387
+ console.warn(`⚠️ [watcher] 拉起失败(观测旁路 best-effort,不影响主流程): ${(e && e.message) || e}`)
1388
+ }
1389
+ }
1304
1390
  // ── 所有权首建 claim(2026-09-14-change-ownership-guards task-02 / FR-01 / D-001@v1)──
1305
1391
  // run <stage> 启动即认领 owner(claimChangeOwner 守卫语义:已有值不覆盖——他人持有不抢
1306
1392
  // 也不拒;拒绝语义只锁接管类操作,见 index.js apply/cleanup/assess 接线)。会话标识三级:
@@ -1652,11 +1738,16 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1652
1738
  const doneAnswer = getFlagValue('--answer')
1653
1739
  // isSkipApply/sessionFlag(task-03):--skip-apply 归档收口跳过 + --session 所有权会话标识,
1654
1740
  // 随 completeStep options 透传(isNoDocs 同款链路)——消费点 archive/quick 收尾 handler。
1741
+ // burst 分发门(2026-09-22-stage-burst-fold D-003@v2/FR-06):白名单主阶段 + readStageBurst
1742
+ // 开启 → completeStepBurst 循环收口(completeStep 本体零改动,守卫逐轮生效);options 原样透传。
1743
+ if (STAGE_BURST_STAGES.includes(stageName) && await readStageBurst(cwd)) {
1744
+ return await completeStepBurst(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, doneAnswer, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, isSkipApply, sessionFlag: getFlagValue('--session'), quickFiles, stepAssert: stepAssertValue })
1745
+ }
1655
1746
  return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, doneAnswer, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, isSkipApply, sessionFlag: getFlagValue('--session'), quickFiles, stepAssert: stepAssertValue })
1656
1747
  }
1657
1748
 
1658
1749
  // 默认:输出当前步骤
1659
- return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isAllowDelete, isForceBaseline, isForceRescan, linkedChanges, linkedChangesAuto: linkedAuto, taskDescription: inputText, adoptBranch: stageName === 'execute' && flags.includes('--adopt-branch') })
1750
+ return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isAllowDelete, isForceBaseline, isForceRescan, linkedChanges, linkedChangesAuto: linkedAuto, taskDescription: inputText, adoptBranch: stageName === 'execute' && flags.includes('--adopt-branch'), sameSession: flags.includes('--same-session') })
1660
1751
  }
1661
1752
 
1662
1753
  /**
@@ -1844,6 +1935,23 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1844
1935
  // 零活跃建变更已上移入口守卫层,多活跃 exit 2 由入口守卫处理)。
1845
1936
  if (changeName) console.log(`🎯 auto 目标变更(单活跃自动选中或显式指定):${changeName}`)
1846
1937
 
1938
+ // ── watcher 拉起(2026-09-23-sentinel-rules task-05 / FR-09)──
1939
+ // run auto 在 runCommand 主干的 spawnWatcher 挂点(effectiveChange 解析后那处)之前
1940
+ // 早退进本函数——auto 路径是 run 族唯一观测缺口。语义同 flow start 接线:best-effort
1941
+ // 失败只 warn 不阻断,SILLYSPEC_WATCHER=0 逃生阀在 spawnWatcher 内短路,活锁合并
1942
+ // coalesced,未连接平台也 spawn(本地 jsonl 是事件唯一真相源)。
1943
+ if (changeName && changeName !== 'default') {
1944
+ try {
1945
+ const { spawnWatcher } = await import('../watcher.js')
1946
+ const r = await spawnWatcher(cwd, changeName, { ...platformOpts, specBase })
1947
+ if (r.status === 'spawned') {
1948
+ console.log(`🔄 [watcher] 观测旁路已拉起(auto 路径补挂,事件恒带 provisional:true): ${changeName}`)
1949
+ }
1950
+ } catch (e) {
1951
+ console.warn(`⚠️ [watcher] 拉起失败(观测旁路 best-effort,不影响主流程): ${(e && e.message) || e}`)
1952
+ }
1953
+ }
1954
+
1847
1955
  // Helper: 在 auto 模式下获取步骤定义
1848
1956
  const getAutoSteps = async (stage) => {
1849
1957
  if (stage === 'brainstorm') {
@@ -1901,8 +2009,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1901
2009
  const localConfig = readAutoModeFromLocalYaml(cwd)
1902
2010
  const classification = classifyChange({ description: inputText || '', explicitMode, localConfig })
1903
2011
  if (classification.mode === 'quick') {
1904
- console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
1905
- console.log(` 此变更建议使用 quick 模式,运行:sillyspec run quick "${inputText || '需求'}"`)
2012
+ console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
2013
+ console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话):`)
2014
+ console.log(` sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"`)
1906
2015
  return
1907
2016
  }
1908
2017
  console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
@@ -1989,16 +2098,24 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1989
2098
  return
1990
2099
  }
1991
2100
 
1992
- if (!outputText) {
2101
+ // burst 分发门(2026-09-22-stage-burst-fold D-003@v2/X-4,FR-06):auto 路径 burst 分支跳过
2102
+ // --output 预合成(预合成文本在 burst 下沦为横幅重复打印——burst 直接透传原始 outputText,
2103
+ // 横幅去重归 completeStepBurst 单点),循环收口后本路径既有推进逻辑(nextPendingIdx 渲染 /
2104
+ // nextInFlow 阶段推进)基于重读 progress 自然衔接。
2105
+ const _autoBurstOn = STAGE_BURST_STAGES.includes(currentStage) && await readStageBurst(cwd)
2106
+
2107
+ if (!outputText && !_autoBurstOn) {
1993
2108
  // P0-2(noai-ir-roadmap §3):auto --done 的 --output 同样可省略——与 completeStep 同源
1994
2109
  // 的事实性摘要合成(此前硬拒 exit 2 是用法守卫;SS-META 的 doneCommand 仍带 --output,
1995
2110
  // 照抄路径零变化,省略路径不再罚一轮)。
1996
2111
  const _autoStep = progress?.stages?.[currentStage]?.steps?.find((s) => s && ['pending', 'in-progress', 'blocked'].includes(s.status))
1997
- outputText = synthesizeStepOutput({ stageName: currentStage, stepName: _autoStep?.name || currentStage, cwd })
2112
+ outputText = synthesizeStepOutput({ stageName: currentStage, stepName: _autoStep?.name || currentStage, cwd, platformOpts })
1998
2113
  console.log(`🤖 auto --done 未带 --output——CLI 已按事实合成步骤摘要(语义说明下次带 --output 手写)。`)
1999
2114
  }
2000
2115
 
2001
- const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
2116
+ const result = _autoBurstOn
2117
+ ? await completeStepBurst(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
2118
+ : await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
2002
2119
  if (!result) return
2003
2120
  progress = pm.read(cwd, changeName)
2004
2121
  // change 行被并发归档/删除时 read 返回 null(体检 BUG-05):引导排查而非 TypeError 崩溃
@@ -2157,7 +2274,7 @@ export async function maybeWaitInteractive(stepDef, changeName, stageName, { rlF
2157
2274
  const { execFileSync } = await import('node:child_process')
2158
2275
  const binSelf = resolveBinSelfPath()
2159
2276
  try {
2160
- execFileSync(process.execPath, [binSelf, 'run', 'auto', '--continue', '--answer', answer.trim(), '--change', changeName], { stdio: 'inherit', cwd: process.cwd() })
2277
+ execFileSync(process.execPath, [binSelf, 'run', 'auto', '--continue', '--answer', answer.trim(), '--change', changeName], { stdio: 'inherit', cwd: process.cwd(), windowsHide: true })
2161
2278
  } catch { /* continue 失败(非 waiting 态等)静默——回三段式由 agent 走 */ }
2162
2279
  }
2163
2280
  } catch (e) {