sillyspec 3.28.0 → 3.28.3

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 (60) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +462 -0
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +418 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +197 -34
  17. package/src/docs-migrate.js +124 -0
  18. package/src/doctor-diagnostics.js +11 -2
  19. package/src/endpoint-baseline.js +109 -0
  20. package/src/git-helper.js +31 -0
  21. package/src/index.js +381 -22
  22. package/src/modules.js +41 -12
  23. package/src/progress/change-registry.js +39 -0
  24. package/src/progress/consistency-doctor.js +124 -5
  25. package/src/progress/stage-machine.js +13 -2
  26. package/src/progress.js +5 -0
  27. package/src/quicklog.js +51 -0
  28. package/src/run/command.js +199 -5
  29. package/src/run/complete-handlers.js +60 -4
  30. package/src/run/complete.js +105 -6
  31. package/src/run/gates.js +353 -2
  32. package/src/run/progress-confirm.js +66 -0
  33. package/src/run/prompt.js +193 -15
  34. package/src/run/quick-audit.js +12 -5
  35. package/src/run/scan-profile.js +117 -7
  36. package/src/run/shared.js +22 -6
  37. package/src/run/stage.js +14 -1
  38. package/src/scan-diff.js +54 -1
  39. package/src/scan-facts.js +173 -0
  40. package/src/scan-postcheck.js +114 -3
  41. package/src/spec-sync.js +19 -2
  42. package/src/stage-review.js +62 -3
  43. package/src/stages/archive.js +5 -5
  44. package/src/stages/brainstorm-auto.js +2 -1
  45. package/src/stages/brainstorm.js +12 -20
  46. package/src/stages/execute.js +19 -21
  47. package/src/stages/plan-postcheck.js +194 -1
  48. package/src/stages/plan.js +8 -3
  49. package/src/stages/quick.js +4 -2
  50. package/src/stages/scan.js +44 -102
  51. package/src/stages/verify.js +18 -18
  52. package/src/task-review.js +200 -0
  53. package/src/taskcard.js +8 -1
  54. package/src/verify-postcheck.js +771 -29
  55. package/src/verify-probes.js +89 -10
  56. package/src/workflow.js +30 -0
  57. package/templates/agents-instruction.md +1 -1
  58. package/templates/prompts/taskcard-rules.md +5 -0
  59. package/templates/workflows/scan-docs.yaml +6 -0
  60. package/.claude/skills/verify-per-user/SKILL.md +0 -6
@@ -23,7 +23,7 @@ import { basename, 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, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
26
+ import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
27
27
  import { resolveQuickLinkedChanges } from './quick-audit.js'
28
28
  import { outputStep, collectStageWaitHistory } from './prompt.js'
29
29
  import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
@@ -107,6 +107,38 @@ function warnMsysMangledFlag(flag, value) {
107
107
  console.error(' 非本意 → 去掉前导 / 或改写表述后重发本命令;确需原样 → 命令前加 MSYS_NO_PATHCONV=1。')
108
108
  }
109
109
 
110
+ /**
111
+ * done-like 步骤动作(--done/--skip/--wait/--continue/--reset/--reopen)的目标 change 在当前
112
+ * 库是否已「物化」(坑 done-phantom-change-silent-create,2026-09-04 事故元凶①)。
113
+ *
114
+ * 背景:接管指针切换/重建进度库后,--change 常指向旧库的变更名;新库里既无 DB 行也无
115
+ * changes/<name>/ 目录,runCommand 的 !progress 分支却无条件 initChange 静默新建——幻影
116
+ * 变更零进度起步,--done 被记到幻影上,原变更进度停滞(本次事故直接元凶)。
117
+ *
118
+ * 物化口径(progress 已知为 null,只查磁盘标记):
119
+ * - 普通变更:changes/<name>/ 存在(archive 已归档目录也算——归档后 DB 行仍可读,
120
+ * 但行被清理的恢复场景认 archive 目录物化,走 initChange 自愈是合法路径);
121
+ * - quick-<8hex> 会话:无 changes/ 目录,物化标记 = 会话 guard(per-session 目录或
122
+ * legacy 单文件——legacy 无法归属会话,保守认物化,宁可漏拦不误杀真会话)。
123
+ *
124
+ * 纯函数:只读磁盘,供 command.js 守卫 + 单测复用。
125
+ * @param {object} opts
126
+ * @param {string|null} opts.target - 目标 change 名(--change 值或 changes/ 目录唯一推导)
127
+ * @param {string} opts.specBase - 当前进度库根(平台 specRoot 或 cwd/.sillyspec)
128
+ * @param {string|null} [opts.quickSessionsDir] - quick 会话 guard 目录(缺省按 specBase/.runtime 推导)
129
+ * @returns {boolean} true = 目标已物化(可走既有 initChange 自愈),false = 幻影(应拒绝新建)
130
+ */
131
+ export function doneLikeTargetMaterialized({ target, specBase, quickSessionsDir = null }) {
132
+ if (!target || !specBase) return false
133
+ if (/^quick-[0-9a-f]{8}$/.test(target)) {
134
+ const rt = quickSessionsDir || join(specBase, '.runtime', 'quick-sessions')
135
+ return existsSync(join(rt, target, 'guard.json'))
136
+ || existsSync(join(specBase, '.runtime', 'quick-guard.json'))
137
+ }
138
+ return existsSync(join(specBase, 'changes', target))
139
+ || existsSync(join(specBase, 'changes', 'archive', target))
140
+ }
141
+
110
142
  /**
111
143
  * 从 progress 或变更目录推导变更名
112
144
  */
@@ -951,6 +983,43 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
951
983
  }
952
984
 
953
985
  if (!progress) {
986
+ // ── done-like 存在性守卫(坑 done-phantom-change-silent-create,2026-09-04 事故元凶①)──
987
+ // --done/--skip/--wait/--continue/--reset/--reopen 都预设「目标 change 已存在于当前进度库」。
988
+ // 此前 !progress 时无条件 initChange 静默新建——接管指针切库/换库后 --change 指向旧库的
989
+ // 变更名,新库里凭空建幻影变更再把动作记上去。目标已物化(changes/ 目录或 quick 会话
990
+ // guard 存在)仍放行走下方 initChange 自愈(DB 重建/迁移后从目录物化是合法恢复路径)。
991
+ if (isDone || isSkip || isWait || isContinue || isReset || isReopen) {
992
+ const actionFlag = isDone ? '--done' : isSkip ? '--skip' : isWait ? '--wait'
993
+ : isContinue ? '--continue' : isReset ? '--reset' : '--reopen'
994
+ const guardTarget = changeName || resolveChangeNameAuto(cwd, specRoot)
995
+ if (!doneLikeTargetMaterialized({
996
+ target: guardTarget,
997
+ specBase,
998
+ quickSessionsDir: resolveQuickSessionsDir(platformOpts, specBase),
999
+ })) {
1000
+ const pointerPath = join(cwd, '.sillyspec-platform.json')
1001
+ const pointerMode = existsSync(pointerPath)
1002
+ let activeList = []
1003
+ try { activeList = pm.listChanges(cwd) } catch { /* 列举失败不掩盖主报错 */ }
1004
+ console.error(`❌ ${actionFlag} 找不到目标变更的进度,拒绝静默新建(防幻影变更)。`)
1005
+ console.error(` 目标变更: ${guardTarget || '(未指定 --change,且当前库无唯一活跃变更)'}`)
1006
+ console.error(` 当前进度库: ${specBase}${pointerMode ? `(平台接管指针 ${pointerPath} 指向此库)` : '(本地模式)'}`)
1007
+ console.error(` 本库活跃变更: ${activeList.length > 0 ? activeList.join('、') : '(无)'}`)
1008
+ console.error(' 排查:')
1009
+ console.error(` ① 变更名是否打错——从上面活跃列表选,或 sillyspec progress show 查看各变更进度`)
1010
+ if (pointerMode) {
1011
+ console.error(` ② 最近是否切换/重建过平台接管指针(重跑平台 scan 会换库)——原变更可能留在旧库:检查 ${pointerPath} 的 specRoot,或用 --spec-dir 显式指回旧库`)
1012
+ } else {
1013
+ console.error(` ② cwd 是否漂移/切库——用 --spec-dir <路径>/.sillyspec 显式指定正确的进度库`)
1014
+ }
1015
+ if (stageName === 'quick' || /^quick-[0-9a-f]{8}$/.test(guardTarget || '')) {
1016
+ console.error(` ③ quick 会话跨进程恢复须带 --change <quick-session-id>;本库无该会话 guard——会话可能属于旧库`)
1017
+ } else {
1018
+ console.error(` ③ 确要新建变更:先 sillyspec run brainstorm --change <名>(不带 ${actionFlag})`)
1019
+ }
1020
+ process.exit(2) // 用法/环境错(目标变更不在当前库)→ exit 2
1021
+ }
1022
+ }
954
1023
  // 如果指定了变更名或有变更目录,自动初始化变更的 progress
955
1024
  const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
956
1025
  if (autoChange) {
@@ -1018,6 +1087,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1018
1087
  // brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
1019
1088
  progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
1020
1089
  changeName = autoName
1090
+ } else if (stageName === 'auto') {
1091
+ // auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
1092
+ // run auto --input——此前此处 exit 2 是断头路。复用 brainstorm 命名(date-new-change-hex)。
1093
+ // pm.read 对零/多活跃都返 null——先分流:多活跃报错列候选(不隐式选择),零活跃才建。
1094
+ const activeCount = pm.listChanges(cwd)
1095
+ if (activeCount.length > 1) {
1096
+ console.error(`❌ 已存在 ${activeCount.length} 个活跃变更,run auto 无法自动定位目标。`)
1097
+ console.error(' 活跃变更:' + activeCount.join('、'))
1098
+ console.error(' 请用 --change <变更名> 显式指定,或先归档/收尾现有变更。')
1099
+ process.exit(2)
1100
+ }
1101
+ const date = new Date().toISOString().slice(0, 10)
1102
+ const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
1103
+ console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
1104
+ progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
1105
+ changeName = autoName
1021
1106
  } else {
1022
1107
  console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
1023
1108
  // 自愈引导(坑 suggestion-command-missing-change 同族):多活跃变更仓不带 --change 时
@@ -1477,8 +1562,15 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1477
1562
  const inputText = autoFlagValue('--input')
1478
1563
  const skipApproval = flags.includes('--skip-approval')
1479
1564
  const explicitMode = autoFlagValue('--mode')
1565
+ // --wait-interactive(2026-09-08-auto-driver D-004@v1,FR-03):TTY 下 requiresUser 步 readline 直收
1566
+ const waitInteractive = autoFlagValue('--wait-interactive') === 'true'
1567
+ globalThis.__ssWaitInteractive = waitInteractive // maybeWaitInteractive 消费(同进程直通)
1480
1568
  const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
1481
1569
 
1570
+ // 单活跃自动选中为既有行为(入口 pm.read(cwd,null))——此处仅回显(D-003@v1;
1571
+ // 零活跃建变更已上移入口守卫层,多活跃 exit 2 由入口守卫处理)。
1572
+ if (changeName) console.log(`🎯 auto 目标变更(单活跃自动选中或显式指定):${changeName}`)
1573
+
1482
1574
  // Helper: 在 auto 模式下获取步骤定义
1483
1575
  const getAutoSteps = async (stage) => {
1484
1576
  if (stage === 'brainstorm') {
@@ -1499,6 +1591,10 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1499
1591
  if (stage === 'brainstorm') {
1500
1592
  const existingSteps = progress.stages?.brainstorm?.steps
1501
1593
  const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '进度确认与上下文加载'
1594
+ // auto 表在位即早退(2026-09-08 E2E 实证 bug:此前仅 !isAutoModeSteps 重种后 return,
1595
+ // auto 表存在时掉进下方通用 ensureStageSteps——主模式 8 步定义判 4 步 auto 表「漂移」
1596
+ // 重种主模式表,与 getAutoSteps 渲染双轨互踩,进度永远回 step1)
1597
+ if (isAutoModeSteps) return progress
1502
1598
  if (!isAutoModeSteps) {
1503
1599
  if (!progress.stages) progress.stages = {}
1504
1600
  progress.stages.brainstorm = {
@@ -1544,6 +1640,8 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1544
1640
  currentStage = firstOpenStage()
1545
1641
  }
1546
1642
  if (!currentStage) {
1643
+ // 重进入且已全完成:复用收尾总结 helper(FR-04)
1644
+ if (changeName) printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
1547
1645
  console.log('All auto flow stages are complete.')
1548
1646
  return
1549
1647
  }
@@ -1587,7 +1685,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1587
1685
  }
1588
1686
  const next = nextInFlow(currentStage)
1589
1687
  if (next) console.log(`${currentStage} is complete. Run: sillyspec run auto --done --output "${currentStage} complete"`)
1590
- else console.log('All auto flow stages are complete.')
1688
+ else {
1689
+ console.log('All auto flow stages are complete.')
1690
+ // 展示路径全完成(阶段已关但用户重进)——复用收尾总结(FR-04)
1691
+ printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
1692
+ }
1591
1693
  return
1592
1694
  }
1593
1695
  // execute 阶段启动前检查审批
@@ -1608,7 +1710,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1608
1710
  }
1609
1711
  }
1610
1712
  }
1611
- await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage))
1713
+ await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage), { changeName })
1714
+ // wait 直通(FR-03):requiresUser 且 TTY 且旗标开 → readline 直收,等价 --continue --answer
1715
+ await maybeWaitInteractive(defSteps[pendingIdx], changeName, currentStage)
1612
1716
  return
1613
1717
  }
1614
1718
 
@@ -1648,13 +1752,15 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1648
1752
  }
1649
1753
  }
1650
1754
  }
1651
- await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage))
1755
+ await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage), { changeName })
1652
1756
  return
1653
1757
  }
1654
1758
 
1655
1759
  const next = nextInFlow(currentStage)
1656
1760
  if (!next) {
1657
1761
  console.log('\nAll auto flow stages are complete.')
1762
+ // 自然收尾点(--done 后 next==null,FR-04):打印收尾总结
1763
+ printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
1658
1764
  return
1659
1765
  }
1660
1766
 
@@ -1726,7 +1832,95 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1726
1832
  }
1727
1833
  }
1728
1834
  }
1729
- await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, next))
1835
+ await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, next), { changeName })
1730
1836
  }
1731
1837
  }
1732
1838
 
1839
+
1840
+ /**
1841
+ * maybeWaitInteractive —— auto 模式 wait 用户输入直通(change: 2026-09-08-auto-driver,D-004@v1,FR-03)。
1842
+ *
1843
+ * 时序:outputStep 渲染返回后调用。条件全满足(旗标开 + requiresUser 四源 + stdin TTY)→
1844
+ * readline 直收一行,等价自动 `--continue --answer <输入>`(复用 completeStep wait→continue 状态机,
1845
+ * 无新状态);任一不满足/异常 → 静默或提示回三段式(fail-open,Grill P2-①时序)。
1846
+ * 仅 runAutoMode 的首渲染点消费(--done 推进路径不重收)。
1847
+ */
1848
+ export async function maybeWaitInteractive(stepDef, changeName, stageName, { rlFactory = null } = {}) {
1849
+ try {
1850
+ if (!globalThis.__ssWaitInteractive) return
1851
+ if (!stepDef) return
1852
+ const { requiresUser } = await import('./prompt.js')
1853
+ // prompt 正文在此不可得——四源三键优先(WAIT_MARKER_RE 源由 SS-META 渲染时已并入,直通用三键近似:
1854
+ // 真需 wait 的步必有标记,纯正文标记步三段式照旧)
1855
+ const needsUser = stepDef.requiresWait === true || stepDef.conditionalWait === true || stepDef.requiresConfirm === true
1856
+ if (!needsUser) return
1857
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1858
+ console.log('ℹ️ --wait-interactive 需要 TTY(当前非交互环境)——按 prompt 的 wait 指令走三段式')
1859
+ return
1860
+ }
1861
+ // rlFactory 为测试缝(注入伪 readline);生产路径用 node:readline/promises 真实 TTY
1862
+ const rl = rlFactory
1863
+ ? rlFactory()
1864
+ : (await import('node:readline/promises')).createInterface({ input: process.stdin, output: process.stdout })
1865
+ console.log('')
1866
+ const answer = await rl.question('⌨️ 本步需要你的输入(直通模式,回答后将自动 --continue --answer):')
1867
+ rl.close()
1868
+ if (answer && answer.trim()) {
1869
+ console.log(`📩 已接收,自动续行:sillyspec run auto --continue --answer "${answer.trim().slice(0, 60)}${answer.trim().length > 60 ? '…' : ''}" ...`)
1870
+ // 复用既有 continue 路径:与 runCommand 的 --continue 同源(子进程自调避免递归锁)
1871
+ const { execFileSync } = await import('node:child_process')
1872
+ const binSelf = new URL('../bin/sillyspec.js', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
1873
+ try {
1874
+ execFileSync(process.execPath, [binSelf, 'run', 'auto', '--continue', '--answer', answer.trim(), '--change', changeName], { stdio: 'inherit', cwd: process.cwd() })
1875
+ } catch { /* continue 失败(非 waiting 态等)静默——回三段式由 agent 走 */ }
1876
+ }
1877
+ } catch (e) {
1878
+ console.log(`ℹ️ wait 直通异常回退三段式:${e && e.message ? e.message : e}`)
1879
+ }
1880
+ }
1881
+
1882
+
1883
+ /**
1884
+ * printAutoCompletionSummary —— auto 流程收尾总结(change: 2026-09-08-auto-driver,D-005@v1,FR-04)。
1885
+ * 挂点:--done 后 next==null 的自然收尾(:1726 区)+ :1616 重进入分支复用。纯 console 无新文件:
1886
+ * 变更名/各阶段状态/tasks 勾选(readPlanCheckboxStatus)/产物存在清单/last-delta 模块清单(缺失跳过)。
1887
+ */
1888
+ function printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase) {
1889
+ try {
1890
+ if (!changeName) return
1891
+ const progress = pm.read(cwd, changeName)
1892
+ console.log('')
1893
+ console.log('═'.repeat(56))
1894
+ console.log(`🏁 auto 流程收尾总结 — ${changeName}`)
1895
+ for (const s of flowStages) {
1896
+ const st = progress.stages?.[s]?.status || '未开始'
1897
+ const icon = st === 'completed' ? '✅' : st === 'in-progress' ? '🔄' : '⬜'
1898
+ console.log(` ${icon} ${s}: ${st}`)
1899
+ }
1900
+ // tasks 勾选
1901
+ try {
1902
+ const changeDirCk = join((specBase || join(cwd, '.sillyspec')), 'changes', changeName)
1903
+ const plan = pm.readPlanCheckboxStatus(changeDirCk)
1904
+ if (plan && plan.total > 0) console.log(` 📋 任务勾选:${plan.checked}/${plan.total}`)
1905
+ } catch { /* 勾选读取 fail-soft */ }
1906
+ // 产物存在清单
1907
+ const changeDirArt = join((specBase || join(cwd, '.sillyspec')), 'changes', changeName)
1908
+ const artifacts = ['proposal.md', 'design.md', 'tasks.md', 'plan.md', 'verify-result.md', 'delta.md']
1909
+ const found = artifacts.filter(a => existsSync(join(changeDirArt, a)))
1910
+ if (found.length > 0) console.log(` 📁 产物:${found.join('、')}`)
1911
+ // last-delta 模块清单(sidecar 缺失跳过)
1912
+ try {
1913
+ const runtimeRoot = join((specBase || join(cwd, '.sillyspec')), '.runtime')
1914
+ const sidecarPath = join(runtimeRoot, 'last-delta.json')
1915
+ if (existsSync(sidecarPath)) {
1916
+ const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
1917
+ if (Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
1918
+ console.log(` 🔁 涉及模块(最近 delta):${sidecar.affectedModules.join('、')}`)
1919
+ }
1920
+ }
1921
+ } catch { /* sidecar 读取 fail-soft */ }
1922
+ console.log('═'.repeat(56))
1923
+ } catch (e) {
1924
+ console.log(`ℹ️ 收尾总结渲染降级:${e && e.message ? e.message : e}`)
1925
+ }
1926
+ }
@@ -31,7 +31,7 @@ import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnch
31
31
  import { stageRegistry } from '../stages/index.js'
32
32
  import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
33
33
  import { printQuickAuditReview, runQuickTestLintGate, printQuickTestLintGate } from './quick-audit.js'
34
- import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
34
+ import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
35
35
  import { getRule } from '../stage-contract-spec.js'
36
36
  import { archiveDestDirName } from '../stage-contract.js'
37
37
 
@@ -432,6 +432,34 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
432
432
  console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
433
433
  return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
434
434
  }
435
+ // ── 归档前 delta.md 自动生成(P3d task-02,fail-soft 零阻断)──
436
+ // 目录移走前在 changes/<name>/ 落一份 Before/Delta/After 三段式快照(四源采集容缺,
437
+ // buildDeltaReport 见 ../archive-delta.js),随归档包留存。失败只 console.error 留痕、
438
+ // 提示 sillyspec delta --change 手动补,绝不抛出——归档主流程既有失败面(rename/git/清理)
439
+ // 一个不叠加。specRoot 同 complete.js :87 口径(specBase 已含 platform specRoot);
440
+ // runtimeRoot 同本文件 archiveWorktreeCleanup 既有用法 resolveRuntimeRoot(platformOpts, specBase)。
441
+ const deltaChangeName = progress.currentChange || changeName
442
+ if (deltaChangeName) {
443
+ try {
444
+ const { buildDeltaReport, writeLastDeltaSidecar } = await import('../archive-delta.js')
445
+ const deltaChangeDir = join(specBase, 'changes', deltaChangeName)
446
+ const deltaResult = buildDeltaReport({
447
+ changeDir: deltaChangeDir,
448
+ specRoot: specBase,
449
+ // project 缺参可 null(loadModuleMap null 降级路径已支持,报告内逐段降级注记)
450
+ project: progress?.project || null,
451
+ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase),
452
+ // IR 回灌 sidecar(2026-09-07-ir-hardening D-006):与手动补跑同源数据
453
+ withSummary: true,
454
+ })
455
+ writeFileSync(join(deltaChangeDir, 'delta.md'), deltaResult.markdown)
456
+ writeLastDeltaSidecar(resolveRuntimeRoot(platformOpts, specBase), deltaResult)
457
+ console.log(`📊 归档前已生成 delta.md: ${join(deltaChangeDir, 'delta.md')}`)
458
+ } catch (e) {
459
+ console.error(`⚠️ 归档前 delta.md 自动生成失败(不阻断归档): ${e.message}`)
460
+ console.error(` 可手动补: sillyspec delta --change ${deltaChangeName} --spec-dir ${specBase}`)
461
+ }
462
+ }
435
463
  // 主仓互斥锁(坑 main-repo-no-mutex 二批):archiveChangeDirectory 改主仓共享状态(目录
436
464
  // rename + 共享 index 的 git add + marker 删除 + worktree 清理),与并行会话的 apply/cleanup
437
465
  // 互踩。exit 钩子兜底:其内部 guard 失败 exit(1) 时锁也会被清(见 withMainRepoLock)。
@@ -907,10 +935,14 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
907
935
  // P0-2(2026-09-02 跨 agent 工单):触及 src/test 的 quick --done 内置 test+lint
908
936
  // 实测门禁——把 CLAUDE.md 规则 8 从 agent 自律下沉为 CLI 卡点(fail → step 回
909
937
  // pending + exit 1,重跑不丢进度;纯 doc/配置与未配置命令自动跳过不阻断)。
938
+ // declaredFiles 兜底(2026-09-07):倒推 B 模式(代码先行 --done 收尾)的文件全部
939
+ // 早于会话启动被基线吸收 → 审计 changedFiles 空 → 此前静默 skip;声明边界触及
940
+ // src/test 时实测仍应跑(quick-f9138c2f 实证)。
910
941
  const gate = await runQuickTestLintGate({
911
942
  cwd,
912
943
  specBase,
913
944
  changedFiles: review.changedFiles,
945
+ declaredFiles: Array.isArray(mergedGuard?.allowedFiles) ? mergedGuard.allowedFiles : [],
914
946
  changeName,
915
947
  })
916
948
  printQuickTestLintGate(gate)
@@ -988,7 +1020,27 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
988
1020
  }
989
1021
 
990
1022
  if (!qlId) {
991
- // 无 ql-ID(guard 缺失或 brownfield 无 quicklogId):补分配后立即完成,不阻断。
1023
+ // 坑 platform-takeover-phantom-progress-db 同日变体:guard 缺失时兜底**优先复用启动
1024
+ // 分配的 ql-ID**(进度库 changes.quicklog_id,quick 启动时 stage.js 回填)——落码
1025
+ // 注释/模块文档的 ql-ID 是启动时就给出的,补分配新号必然制造引用劈叉(同一次会话
1026
+ // 两个 ID、两份 QUICKLOG 各半)。ID 的条目不在当前 specBase(启动/完成分裂库形态)
1027
+ // → 用原 ID 补建骨架,完成态落在同一 ID 上。库中也无(真 brownfield/极老会话)才
1028
+ // 补分配。
1029
+ try {
1030
+ const dbQlId = pm.getQuicklogId(cwd, changeName)
1031
+ if (dbQlId) {
1032
+ const re = await appendQuicklogEntryWithId(specBase, gitUser, dbQlId, {
1033
+ description: guard?.taskDescription || '(guard 缺失补建)',
1034
+ linkedChanges,
1035
+ allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
1036
+ })
1037
+ qlId = dbQlId
1038
+ console.log(`📝 QUICKLOG 兜底补写复用启动 ql-ID: ${qlId}(guard 缺失,ID 取自进度库 quicklog_id${re.existed ? '' : ',条目已补建'})`)
1039
+ }
1040
+ } catch { /* 读取/补建失败退回补分配,不阻断 */ }
1041
+ }
1042
+ if (!qlId) {
1043
+ // 无 ql-ID(库中也未回填,真 brownfield):补分配后立即完成,不阻断。
992
1044
  try {
993
1045
  const alloc = await allocateQuicklogEntry(specBase, gitUser, {
994
1046
  description: guard?.taskDescription || '(补分配)',
@@ -1530,7 +1582,9 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
1530
1582
  try { unlinkSync(platformOptsFile) } catch {}
1531
1583
 
1532
1584
  // CLI 层 post-check(替代旧的简单检查)
1533
- const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
1585
+ const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult, stampScanDocHeaders } = await import('../scan-postcheck.js')
1586
+ // ② frontmatter CLI 盖章(2026-09-05,幂等兜底——正常已在 scanFinalize / quick postcheck 盖过)
1587
+ stampScanDocHeaders({ cwd, specDir: platformOpts.specRoot, mode: stageData.scanProfile?.mode })
1534
1588
  const postResult = runScanPostCheck({
1535
1589
  cwd,
1536
1590
  specDir: platformOpts.specRoot,
@@ -1613,7 +1667,9 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
1613
1667
 
1614
1668
  // 非 platform 模式 scan 也做轻量 post-check + 结构化输出
1615
1669
  if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
1616
- const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
1670
+ const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult, stampScanDocHeaders } = await import('../scan-postcheck.js')
1671
+ // ② frontmatter CLI 盖章(2026-09-05,幂等兜底)
1672
+ stampScanDocHeaders({ cwd, specDir: null, mode: stageData.scanProfile?.mode })
1617
1673
  const postResult = runScanPostCheck({ cwd, specDir: null, outputText, scanProfile: stageData.scanProfile || null })
1618
1674
  printScanPostCheckResult(postResult)
1619
1675
  // 结构化结果写入 .sillyspec/.runtime/
@@ -21,12 +21,31 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } fr
21
21
  import { writeAtomicSync } from '../fs-atomic.js'
22
22
  import { withFileLock } from '../quicklog.js'
23
23
  import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
24
- import { executeScanPreflight, executeScanPostcheck, computeScanProfile } from './scan-profile.js'
24
+
25
+ // auto 模式 brainstorm 步骤表感知解析(2026-09-08 E2E 实证 bug:--done/--wait 推进主模式 8 步表,
26
+ // 与 runAutoMode getAutoSteps 的 4 步 auto 表双轨互踩——ensureAutoStage 判非 auto 表重种清零,
27
+ // 进度永远回 step1)。DB 现存 brainstorm 表为 auto 形态(4 步+首名「进度确认与上下文加载」)时,
28
+ // complete 路径与渲染路径同源用 brainstormAutoDef.steps。
29
+ async function getStageStepsAutoAware(stageName, cwd, progress, specDir) {
30
+ const steps = await getStageSteps(stageName, cwd, progress, specDir)
31
+ if (stageName !== 'brainstorm') return steps
32
+ const existing = progress?.stages?.brainstorm?.steps
33
+ const isAutoTable = Array.isArray(existing) && existing.length === 4
34
+ && existing[0]?.name === '进度确认与上下文加载'
35
+ if (!isAutoTable) return steps
36
+ const { definition: brainstormAutoDef } = await import('../stages/brainstorm-auto.js')
37
+ return brainstormAutoDef.steps
38
+ }
39
+
40
+ import { executeScanPreflight, executeScanPostcheck, computeScanProfile, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
41
+ import { executeProgressConfirm } from './progress-confirm.js'
42
+ import { AUXILIARY_STAGES } from '../constants.js'
25
43
  import { executePlanPostcheck as runPlanPostcheckLib } from '../stages/plan-postcheck.js'
26
44
  import { outputStep, collectStageWaitHistory } from './prompt.js'
27
45
  import { enforceDepsGate, enforceReviewJsonGate, enforceSymbolImpactGate, warnMissingUiPrototype, completeStageGates, readDesignScale } from './gates.js'
28
46
  import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
29
47
  import { formatExecuteSummary } from '../worktree-apply.js'
48
+ import { validateDecisionModuleRefs } from '../design-facts.js'
30
49
  import { isEndToEndTaskText } from '../change-risk-profile.js'
31
50
  import { deriveTitleFromLinkedChange } from '../quicklog.js'
32
51
  import { gitQuiet } from '../git-helper.js'
@@ -219,7 +238,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
219
238
  }
220
239
 
221
240
  // ── requiresWait 硬门控 ──
222
- const defStepsForCurrent = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
241
+ const defStepsForCurrent = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
223
242
  // def↔DB 一致性守卫(坑 execute-step-table-drift,2026-08-20 实证):runCommand 入口已按名
224
243
  // 重播种(ensureStageSteps),但同一命令进程内 plan.md 若再次被改(或绕过 runCommand 直调
225
244
  // completeStep 的路径),def 与 DB 步数错位会让 currentIdx 的门控/prompt 施加到错误的步骤上
@@ -279,6 +298,60 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
279
298
  await enforceSymbolImpactGate(stageName, changeName, steps[currentIdx]?.name, specBase)
280
299
  // UI 原型软提醒(非阻断):brainstorm 收尾步 design.md 命中前端文件但无 prototype → 提示用户可否决
281
300
  warnMissingUiPrototype(stageName, changeName, steps[currentIdx]?.name, specBase)
301
+ // 决策模块域核验硬门(task-02 接线 design-facts,FR-02/D-002):brainstorm「生成规范文件」步
302
+ // --done 时核验 decisions.md 模块域引用。ERROR(decision_module_ref_invalid,消息已内嵌前缀)
303
+ // = 模块幻觉/书写错误 → fail-closed exit 1(进度不推进,步骤保持 pending);WARNING
304
+ // (decision_module_domain_gap 声明×实改差异)→ warn 放行留人工裁量;skipped(无决策文件/
305
+ // 无 _module-map.yaml 索引)→ 单行 info 放行(无索引不误拦)。fail-soft:核验自身异常 →
306
+ // fail-open(warn 留痕后放行——门禁代码缺陷不应误拦真实完成,与「无索引跳过」同向)。
307
+ // project 解析与 handleWorkflowPostCheck 同链:progress.project(平台模式真实项目名)>
308
+ // steps[idx].project > null(brainstorm 非 perProject 展开,后两位常规为空)。
309
+ if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('生成规范文件') && changeName) {
310
+ try {
311
+ const _dmProject = progress?.project || steps[currentIdx]?.project || null
312
+ const _dmResult = validateDecisionModuleRefs({
313
+ changeDir: join(specBase, 'changes', changeName),
314
+ specRoot: specBase,
315
+ project: _dmProject,
316
+ })
317
+ if ((_dmResult.errors || []).length > 0) {
318
+ console.error(`❌ ── 决策模块域核验阻断(本次 --done 未完成,进度未推进)──`)
319
+ for (const _dmErr of _dmResult.errors) console.error(` • ${_dmErr}`)
320
+ console.error(` 修复:更正 decisions.md 对应条目的「模块域」字段(模块 ID 取自 docs/${_dmProject || '<项目>'}/modules/_module-map.yaml),或以 NEW:<名> 前缀声明新模块后重跑:`)
321
+ console.error(` sillyspec run brainstorm --done${changeName ? ` --change ${changeName}` : ''} --output "修复说明"`)
322
+ process.exit(1)
323
+ }
324
+ for (const _dmWarn of (_dmResult.warnings || [])) console.warn(` ⚠️ ${_dmWarn}`)
325
+ if (_dmResult.skipped) {
326
+ console.log(`ℹ️ [decision_module_check_skipped] ${_dmResult.skipped}`)
327
+ }
328
+ } catch (_dmEx) {
329
+ console.warn(` ⚠️ 决策模块域核验自身异常,fail-open 放行(不误拦本次完成):${_dmEx && _dmEx.message ? _dmEx.message : _dmEx}`)
330
+ }
331
+ }
332
+
333
+ // ── design.md 文件清单行级核验(2026-09-07-ir-hardening D-004@v1,FR-02)──
334
+ // 幻觉路径 ERROR(design_file_ref_invalid,与 plan 侧 validateTargetFiles 同语义先例);
335
+ // NEW: 前缀豁免 / glob 跳过 / 清单缺失 WARNING。挂点与决策模块域核验同点位(brainstorm 末步)。
336
+ if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('生成规范文件') && changeName) {
337
+ try {
338
+ const { validateDesignFileList } = await import('../design-facts.js')
339
+ const _dflResult = validateDesignFileList({
340
+ changeDir: join(specBase, 'changes', changeName),
341
+ cwd,
342
+ })
343
+ if ((_dflResult.errors || []).length > 0) {
344
+ console.error(`❌ ── design 文件清单核验阻断(本次 --done 未完成,进度未推进)──`)
345
+ for (const _dflErr of _dflResult.errors) console.error(` • ${_dflErr.message}`)
346
+ console.error(` 修复:更正 design.md「文件变更清单」对应行路径,或计划新建文件加 NEW: 前缀后重跑:`)
347
+ console.error(` sillyspec run brainstorm --done${changeName ? ` --change ${changeName}` : ''} --output "修复说明"`)
348
+ process.exit(1)
349
+ }
350
+ for (const _dflWarn of (_dflResult.warnings || [])) console.warn(` ⚠️ ${_dflWarn}`)
351
+ } catch (_dflEx) {
352
+ console.warn(` ⚠️ design 清单核验自身异常,fail-open 放行(不误拦本次完成):${_dflEx && _dflEx.message ? _dflEx.message : _dflEx}`)
353
+ }
354
+ }
282
355
 
283
356
  // ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
284
357
  // 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
@@ -296,8 +369,17 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
296
369
  await executeScanPreflight(cwd, platformOpts, stageData.scanProfile || computeScanProfile(cwd, platformOpts))
297
370
  } else if (_cliAction === 'scanPostcheck') {
298
371
  await executeScanPostcheck(cwd, platformOpts, stageData.scanProfile || computeScanProfile(cwd, platformOpts))
372
+ } else if (_cliAction === 'scanDetectProjects') {
373
+ await executeScanDetectProjects(cwd, platformOpts)
374
+ } else if (_cliAction === 'scanResumeCheck') {
375
+ await executeScanResumeCheck(cwd, platformOpts)
376
+ } else if (_cliAction === 'scanFinalize') {
377
+ await executeScanFinalize(cwd, platformOpts)
299
378
  } else if (_cliAction === 'planPostcheck') {
300
379
  await runPlanPostcheckLib({ cwd, specRoot: platformOpts?.specRoot, resolveChangeDir, progress })
380
+ } else if (_cliAction === 'progressConfirm') {
381
+ // 与 stage.js noAI 分支同语义(brainstorm/execute/verify step1 进度确认)
382
+ executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
301
383
  } else {
302
384
  throw new Error(`noAI 步骤 ${steps[currentIdx].name} 的未知 _cliAction: ${_cliAction}——请在 complete.js 注册对应分支`)
303
385
  }
@@ -445,6 +527,11 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
445
527
  }
446
528
  }
447
529
 
530
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
531
+ // `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
532
+ // currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
533
+ // (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
534
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
448
535
  stageData.status = 'completed'
449
536
  stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
450
537
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
@@ -507,6 +594,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
507
594
  console.log(`\n👉 下一步:sillyspec run verify${changeName ? ` --change ${changeName}` : ''}(验证通过后才能归档)`)
508
595
  }
509
596
  } else if (stageName === 'archive') {
597
+ // 机械暂存 CLI 化(2026-09-05 全流程审计):原步骤 6 的 3 条固定路径 git add agent 指令
598
+ // 改为完成时 CLI 直做(未变更路径 git add 是无害 no-op)。平台模式跳过(specRoot 在源码仓外)。
599
+ if (!platformOpts?.specRoot && !platformOpts?.runtimeRoot) {
600
+ const { stageArchiveArtifacts } = await import('../git-helper.js')
601
+ const r = stageArchiveArtifacts(cwd)
602
+ if (r.staged.length > 0) console.log(` 📦 已暂存归档产物(${r.staged.length} 个路径,未提交,由统一提交工具处理)`)
603
+ }
510
604
  console.log('\n👉 归档完成!现在可以提交了:git commit -m "..."')
511
605
  } else if (stageName === 'verify') {
512
606
  // verify 的"验证通过"提示延后到下方 validator 通过后才打印,
@@ -561,7 +655,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
561
655
  }
562
656
  }
563
657
 
564
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
658
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
565
659
  console.log(`✅ Step ${currentIdx + 1}/${steps.length} 完成:${steps[currentIdx].name}\n`)
566
660
 
567
661
  // Workflow post_check(W6 Step6 抽至 complete-handlers.js handleWorkflowPostCheck)
@@ -962,7 +1056,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
962
1056
 
963
1057
  // maxWaitRounds 硬上限:达到后拒绝继续 --wait
964
1058
  const currentStep = stageData.steps[currentIdx]
965
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1059
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
966
1060
  const stepDef = defSteps?.[currentIdx] || {}
967
1061
  const maxWaitRounds = currentStep.maxWaitRounds ?? stepDef.maxWaitRounds
968
1062
  const currentWaitRound = currentStep.waitRound || 0
@@ -1076,7 +1170,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
1076
1170
  } else {
1077
1171
  currentIdx = waitingSteps[0].idx
1078
1172
  }
1079
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1173
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1080
1174
  const currentStepDef = defSteps?.[currentIdx] || {}
1081
1175
  const currentStep = stageData.steps[currentIdx]
1082
1176
  const isRepeatableWait = currentStepDef.repeatableWait === true || currentStep.repeatableWait === true
@@ -1174,6 +1268,11 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
1174
1268
  if (nextPendingIdx === -1 && nextWaitingIdx === -1) {
1175
1269
  stageData.status = 'completed'
1176
1270
  stageData.completedAt = now
1271
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
1272
+ // `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
1273
+ // currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
1274
+ // (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
1275
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
1177
1276
  // persist _write 移到 completeStageGates 成功之后(task-03 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
1178
1277
  // 阶段完成收尾共享管线(含 execute worktree cleanup),消除 continueStep 完成分支绕过 gate 的 S2(task-01 抽出)。
1179
1278
  // gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(与 completeStep 同语义)。
@@ -1258,7 +1357,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
1258
1357
  process.exit(1)
1259
1358
  }
1260
1359
 
1261
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1360
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1262
1361
  const stepDef = defSteps ? defSteps[currentIdx] : null
1263
1362
  if (stepDef && !stepDef.optional) {
1264
1363
  console.error(`❌ 步骤 "${steps[currentIdx].name}" 不可跳过`)