sillyspec 3.28.2 → 3.28.4

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 (67) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +16 -55
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-continue/SKILL.md +8 -38
  4. package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +19 -249
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-propose/SKILL.md +6 -77
  8. package/.claude/skills/sillyspec-resume/SKILL.md +8 -57
  9. package/.claude/skills/sillyspec-state/SKILL.md +7 -46
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +14 -156
  11. package/AGENTS.md +24 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/src/archive-delta.js +127 -2
  15. package/src/change-delete.js +2 -1
  16. package/src/change-list.js +7 -2
  17. package/src/change-risk-profile.js +77 -6
  18. package/src/config-cat.js +12 -1
  19. package/src/constants.js +9 -1
  20. package/src/design-facts.js +64 -0
  21. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  22. package/src/docs-check.js +153 -27
  23. package/src/docs-migrate.js +124 -0
  24. package/src/doctor-diagnostics.js +441 -5
  25. package/src/foreign-declared.js +53 -2
  26. package/src/index.js +353 -28
  27. package/src/module-impact.js +77 -12
  28. package/src/plan-adopt-waves.js +13 -5
  29. package/src/progress/change-registry.js +35 -0
  30. package/src/progress.js +64 -1
  31. package/src/quick-recommend.js +6 -1
  32. package/src/quicklog.js +10 -2
  33. package/src/run/command.js +159 -13
  34. package/src/run/complete-handlers.js +162 -9
  35. package/src/run/complete.js +104 -5
  36. package/src/run/gates.js +116 -8
  37. package/src/run/progress-confirm.js +66 -0
  38. package/src/run/prompt.js +235 -36
  39. package/src/run/quick-audit.js +12 -5
  40. package/src/run/scan-profile.js +20 -2
  41. package/src/run/shared.js +84 -11
  42. package/src/run/stage.js +42 -0
  43. package/src/runtime-hygiene.js +84 -0
  44. package/src/scan-postcheck.js +5 -1
  45. package/src/spec-sync.js +63 -8
  46. package/src/stage-contract.js +83 -3
  47. package/src/stage-review.js +3 -0
  48. package/src/stages/archive.js +4 -4
  49. package/src/stages/brainstorm-auto.js +2 -1
  50. package/src/stages/brainstorm.js +11 -25
  51. package/src/stages/doctor.js +41 -515
  52. package/src/stages/execute.js +10 -14
  53. package/src/stages/plan-postcheck.js +191 -7
  54. package/src/stages/plan.js +7 -27
  55. package/src/stages/quick.js +15 -13
  56. package/src/stages/verify.js +23 -21
  57. package/src/sync-noise.js +158 -0
  58. package/src/sync.js +42 -17
  59. package/src/task-review.js +225 -1
  60. package/src/verify-facts-schema.js +141 -0
  61. package/src/verify-postcheck.js +421 -49
  62. package/src/verify-probes.js +142 -13
  63. package/src/workflow.js +6 -0
  64. package/src/worktree-apply.js +51 -13
  65. package/src/worktree.js +5 -1
  66. package/templates/agents-instruction.md +1 -1
  67. package/.claude/skills/verify-per-user/SKILL.md +0 -6
@@ -26,12 +26,12 @@ import { basename, join, resolve, relative, isAbsolute } from 'node:path'
26
26
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
27
27
  import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
28
28
  import { gitQuiet } from '../git-helper.js'
29
- import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, mergeQuickBoundaryFiles } from './shared.js'
29
+ import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, isQuicklogFileLineNoise, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, mergeQuickBoundaryFiles } from './shared.js'
30
30
  import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
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, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
34
+ import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult, parseFileNotes, getQuickFileNotes } from '../quicklog.js'
35
35
  import { getRule } from '../stage-contract-spec.js'
36
36
  import { archiveDestDirName } from '../stage-contract.js'
37
37
 
@@ -121,11 +121,115 @@ export function findAlreadyArchivedDir(archiveDir, changeName) {
121
121
  return null
122
122
  }
123
123
 
124
+ /**
125
+ * reviewedFiles[0] → 归属变更名。契约为 `changes/<change>/<doc>`(stage-review.js
126
+ * getLatestStageReviewRunId)。取 `changes/` 后第一路径段精确相等——`includes('changes/'+name+'/')`
127
+ * 会让短名 login 命中 2026-08-01-login(坑 marker-suffix-overmatch 同类)。
128
+ * @returns {string|null}
129
+ */
130
+ function ownerFromReviewedFiles(reviewedFiles) {
131
+ const r0 = Array.isArray(reviewedFiles) ? String(reviewedFiles[0] || '') : ''
132
+ const m = r0.replace(/\\/g, '/').match(/(?:^|\/)changes\/([^/]+)\//)
133
+ return m ? m[1] : null
134
+ }
135
+
136
+ /**
137
+ * 归档后按 change 精确回收 .runtime 取证(ql-20260908-005-7549)。
138
+ *
139
+ * 调用时点:handleArchiveConfirmStep 已把 reconcile/apply-pathspec 吃进 delta.md 之后
140
+ * (fail-soft 生成失败也不阻断——变更已终态,runtime 取证无读者)。自愈归档 / quick 轻量
141
+ * 归档 / change-delete 无 delta 同样可删。
142
+ *
143
+ * 归属 fail-closed(禁 mtime 猜、禁后缀匹配):
144
+ * apply-pathspec-<change>.txt 精确文件名
145
+ * execute-runs/<runId>/ 仅当 `change` 戳全等(无戳旧 run 留下,不按覆盖度启发式删)
146
+ * stage-reviews/<dir>/ 仅当 review.json reviewedFiles 首段 === changeName
147
+ * verify-runs/<ts>/ 仅当目录内 JSON 的 change 字段集合 size=1 且等于本变更
148
+ * 本轮不扩 endpoint-baselines / contract-artifacts / last-delta.json。
149
+ *
150
+ * fail-open:卫生动作失败不抛,返回 { ok:true }。
151
+ *
152
+ * @param {string} runtimeRoot
153
+ * @param {string} changeName
154
+ * @returns {{ ok: true, removed: number }}
155
+ */
156
+ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
157
+ if (!runtimeRoot || !changeName || !existsSync(runtimeRoot)) return { ok: true, removed: 0 }
158
+ let removed = 0
159
+ const gone = (p, recursive = false) => {
160
+ try {
161
+ if (recursive) rmSync(p, { recursive: true, force: true })
162
+ else unlinkSync(p)
163
+ removed++
164
+ } catch { /* 单份失败不连坐,下次归档/删变更再收 */ }
165
+ }
166
+
167
+ try {
168
+ const pathspec = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
169
+ if (existsSync(pathspec)) gone(pathspec)
170
+ } catch {}
171
+
172
+ try {
173
+ const execDir = join(runtimeRoot, 'execute-runs')
174
+ if (existsSync(execDir)) {
175
+ for (const runId of readdirSync(execDir)) {
176
+ const stampFile = join(execDir, runId, 'change')
177
+ if (!existsSync(stampFile)) continue
178
+ let stamp = ''
179
+ try { stamp = readFileSync(stampFile, 'utf8').trim() } catch { continue }
180
+ if (stamp !== changeName) continue
181
+ gone(join(execDir, runId), true)
182
+ }
183
+ }
184
+ } catch {}
185
+
186
+ try {
187
+ const srDir = join(runtimeRoot, 'stage-reviews')
188
+ if (existsSync(srDir)) {
189
+ for (const entry of readdirSync(srDir, { withFileTypes: true })) {
190
+ if (!entry.isDirectory()) continue
191
+ const rjPath = join(srDir, entry.name, 'review.json')
192
+ if (!existsSync(rjPath)) continue
193
+ let owner = null
194
+ try {
195
+ const rj = JSON.parse(readFileSync(rjPath, 'utf8'))
196
+ owner = ownerFromReviewedFiles(rj.reviewedFiles)
197
+ } catch { continue }
198
+ if (owner !== changeName) continue
199
+ gone(join(srDir, entry.name), true)
200
+ }
201
+ }
202
+ } catch {}
203
+
204
+ try {
205
+ const vrDir = join(runtimeRoot, 'verify-runs')
206
+ if (existsSync(vrDir)) {
207
+ for (const entry of readdirSync(vrDir, { withFileTypes: true })) {
208
+ if (!entry.isDirectory()) continue
209
+ const dir = join(vrDir, entry.name)
210
+ let files = []
211
+ try { files = readdirSync(dir).filter(f => f.endsWith('.json')) } catch { continue }
212
+ const owners = new Set()
213
+ for (const f of files) {
214
+ try {
215
+ const data = JSON.parse(readFileSync(join(dir, f), 'utf8'))
216
+ if (data && typeof data.change === 'string' && data.change) owners.add(data.change)
217
+ } catch { /* 单文件坏 JSON 跳过,不据此猜归属 */ }
218
+ }
219
+ if (owners.size === 1 && owners.has(changeName)) gone(dir, true)
220
+ }
221
+ }
222
+ } catch {}
223
+
224
+ return { ok: true, removed }
225
+ }
226
+
124
227
  /**
125
228
  * 归档时清理可能残留的 worktree(execute 自动清理未走到 / 有未 apply 变更被遗弃)。
126
229
  * 安全策略:有未 apply 变更时保留 worktree 并警告,避免误删用户未应用的代码。
127
230
  * 同时清理该变更的 execute / stage-review runId marker(只写不删的累积物,
128
- * 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)。
231
+ * 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)
232
+ * 以及归档后无读者的 runtime 取证(pruneArchivedChangeRuntime)。
129
233
  * 从 archiveChangeDirectory 抽出,供正常归档 + 自愈归档复用。
130
234
  */
131
235
  export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts = {}) {
@@ -151,6 +255,12 @@ export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, p
151
255
  if (markers.length > 0) {
152
256
  console.log(`🧹 归档清理 ${markers.length} 个 runId marker(execute/stage-review)`)
153
257
  }
258
+ // 取证回收必须在下方 worktree 块的 `if (!meta) return` 之前:无 meta 早退是常态
259
+ // (apply 已 cleanup),漏接就会让 execute-runs/verify-runs 继续按变更数累积。
260
+ const pruned = pruneArchivedChangeRuntime(runtimeRoot, archiveChangeName)
261
+ if (pruned.removed > 0) {
262
+ console.log(`🧹 归档回收 ${pruned.removed} 项 runtime 取证(execute-runs/stage-reviews/verify-runs/apply-pathspec,按 change 精确匹配)`)
263
+ }
154
264
  }
155
265
  } catch (e) {
156
266
  console.warn(`⚠️ 归档 runId marker 清理失败(不阻断归档): ${e.message}`)
@@ -441,16 +551,19 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
441
551
  const deltaChangeName = progress.currentChange || changeName
442
552
  if (deltaChangeName) {
443
553
  try {
444
- const { buildDeltaReport } = await import('../archive-delta.js')
554
+ const { buildDeltaReport, writeLastDeltaSidecar } = await import('../archive-delta.js')
445
555
  const deltaChangeDir = join(specBase, 'changes', deltaChangeName)
446
- const deltaMd = buildDeltaReport({
556
+ const deltaResult = buildDeltaReport({
447
557
  changeDir: deltaChangeDir,
448
558
  specRoot: specBase,
449
559
  // project 缺参可 null(loadModuleMap null 降级路径已支持,报告内逐段降级注记)
450
560
  project: progress?.project || null,
451
561
  runtimeRoot: resolveRuntimeRoot(platformOpts, specBase),
562
+ // IR 回灌 sidecar(2026-09-07-ir-hardening D-006):与手动补跑同源数据
563
+ withSummary: true,
452
564
  })
453
- writeFileSync(join(deltaChangeDir, 'delta.md'), deltaMd)
565
+ writeFileSync(join(deltaChangeDir, 'delta.md'), deltaResult.markdown)
566
+ writeLastDeltaSidecar(resolveRuntimeRoot(platformOpts, specBase), deltaResult)
454
567
  console.log(`📊 归档前已生成 delta.md: ${join(deltaChangeDir, 'delta.md')}`)
455
568
  } catch (e) {
456
569
  console.error(`⚠️ 归档前 delta.md 自动生成失败(不阻断归档): ${e.message}`)
@@ -897,6 +1010,21 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
897
1010
  console.log(`🛡️ quick 边界已追加: ${added.length} 个文件(累计 ${guard.allowedFiles.length} 个): ${added.join(', ')}`)
898
1011
  }
899
1012
  }
1013
+ // --file-notes 路径同源并入边界(2026-09-08 用户反馈①:step3 只带 --file-notes 未带
1014
+ // --files 时,QUICKLOG 文件行写进了这些文件、审计却按「超出 allowedFiles + 未声明」
1015
+ // 拦——两条声明通道口径打架)。--file-notes 是显式的改动文件声明(路径部分参与落盘
1016
+ // 文件行),与 --files 同语义追加;括注只是展示不参与。读取用 getQuickFileNotes(与
1017
+ // 下方 completeQuicklogEntry 消费同一 per-process 值,读不清)。
1018
+ {
1019
+ const fileNotePaths = parseFileNotes(getQuickFileNotes()).map(n => n.path).filter(Boolean)
1020
+ if (guard && fileNotePaths.length > 0) {
1021
+ const { added } = mergeQuickBoundaryFiles(guard, fileNotePaths, cwd)
1022
+ if (added.length > 0) {
1023
+ try { writeAtomicSync(sessionGuardFile, JSON.stringify(guard, null, 2)) } catch { /* 同上:内存合并值兜底 */ }
1024
+ console.log(`🛡️ quick 边界已追加(--file-notes 声明与文件行同源): ${added.length} 个文件(累计 ${guard.allowedFiles.length} 个): ${added.join(', ')}`)
1025
+ }
1026
+ }
1027
+ }
900
1028
  // 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
901
1029
  // task-02:mergedGuard 提升到 if 外声明,供下方并发预检钩子复用与 auditQuickCompletion
902
1030
  // 同源的 guard 字段(baselineFiles/linkedChanges)。brownfield 无 guard 时保持 null,
@@ -932,10 +1060,14 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
932
1060
  // P0-2(2026-09-02 跨 agent 工单):触及 src/test 的 quick --done 内置 test+lint
933
1061
  // 实测门禁——把 CLAUDE.md 规则 8 从 agent 自律下沉为 CLI 卡点(fail → step 回
934
1062
  // pending + exit 1,重跑不丢进度;纯 doc/配置与未配置命令自动跳过不阻断)。
1063
+ // declaredFiles 兜底(2026-09-07):倒推 B 模式(代码先行 --done 收尾)的文件全部
1064
+ // 早于会话启动被基线吸收 → 审计 changedFiles 空 → 此前静默 skip;声明边界触及
1065
+ // src/test 时实测仍应跑(quick-f9138c2f 实证)。
935
1066
  const gate = await runQuickTestLintGate({
936
1067
  cwd,
937
1068
  specBase,
938
1069
  changedFiles: review.changedFiles,
1070
+ declaredFiles: Array.isArray(mergedGuard?.allowedFiles) ? mergedGuard.allowedFiles : [],
939
1071
  changeName,
940
1072
  })
941
1073
  printQuickTestLintGate(gate)
@@ -1061,14 +1193,16 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1061
1193
  // 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
1062
1194
  // resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
1063
1195
  try {
1064
- // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
1065
- // 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
1196
+ // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime 等),
1197
+ // 过滤掉只留真实业务文件——但**模块卡 + changelog sidecar 保留**(isQuicklogFileLineNoise,
1198
+ // 2026-09-08 用户反馈①:收尾必改项此前每回手工补录;审计豁免面与记录面分叉)。brownfield
1199
+ // 无 review → 空数组,文件行不动。
1066
1200
  // 归属口径(2026-08-18 误归属修复):声明会话优先 attributedFiles(窗口∩声明∪同文件并发命中),
1067
1201
  // 他者窗口文件不进文件行;未声明会话/旧 review 无 attributedFiles → 兜底 changedFiles 全量。
1068
1202
  const auditFiles = Array.isArray(review?.attributedFiles)
1069
1203
  ? review.attributedFiles
1070
1204
  : (Array.isArray(review?.changedFiles) ? review.changedFiles : [])
1071
- const realFiles = auditFiles.filter(f => !isQuickMetadata(f, linkedChanges))
1205
+ const realFiles = auditFiles.filter(f => !isQuicklogFileLineNoise(f, linkedChanges))
1072
1206
  // D-8 落盘(2026-08-18 修):advisory 欠账信号从「纯打印」升级为「随条目落盘」——修复
1073
1207
  // 「欠账已记录(QUICKLOG reasons)」的不实承诺(交叉审查实证 reasons 纯 stdout,事后不可审计)。
1074
1208
  // 两周实测(2026-08-31 裁决,doc-consistency-debt §七)需要分母:信号触发次数必须可追溯。
@@ -1080,6 +1214,25 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1080
1214
  }
1081
1215
  if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
1082
1216
  auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
1217
+ // 行号漂移自动重锚(2026-09-08 用户反馈②:活文档 file:line 硬编码随任何插入失效,
1218
+ // 每回手跑 docs check --fix 是机械活):autoReanchorDocRefs = --fix 主链路编程化封装
1219
+ // (fixable 唯一/优选命中才改 + 定点替换 + 同口径回执),只作用于本次改动的文档。
1220
+ // fail-open:重锚任何异常只退化为上方 advisory 文案,绝不阻断 --done。
1221
+ try {
1222
+ const deletedSet = new Set(review.deletedFiles || [])
1223
+ const mdChanged = (review.changedFiles || [])
1224
+ .filter(f => f.endsWith('.md') && !deletedSet.has(f) && !isQuickMetadata(f, linkedChanges))
1225
+ if (mdChanged.length > 0) {
1226
+ const { autoReanchorDocRefs, readDocsCheckConfig } = await import('../docs-check.js')
1227
+ let fixCfg = {}
1228
+ try { fixCfg = readDocsCheckConfig(cwd) || {} } catch { fixCfg = {} }
1229
+ const r = autoReanchorDocRefs(cwd, mdChanged, fixCfg)
1230
+ if (r.applied > 0) {
1231
+ auditNotes.push(`🔧 行号漂移已自动重锚 ${r.applied} 处(同口径复跑:${r.invalidBefore} → ${r.invalidAfter};剩余 ${r.remaining} 处需人工 sillyspec docs check)`)
1232
+ console.log(`🔧 文档行号漂移自动重锚:${r.applied} 处已改写,剩余 ${r.remaining} 处需人工(sillyspec docs check 可复现)`)
1233
+ }
1234
+ }
1235
+ } catch { /* 自动重锚失败退化为纯 advisory(上方 auditNote 已落盘) */ }
1083
1236
  }
1084
1237
  // 归属切分注(2026-08-18 误归属修复):窗口内未声明脏文件不进「文件:」行,但必须落盘可追溯
1085
1238
  // (多 agent 并发仓他者窗口改动 / 本会话漏声明均可能),防真实改动被静默挤走。
@@ -21,7 +21,25 @@ 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
+
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
+
24
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'
@@ -220,7 +238,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
220
238
  }
221
239
 
222
240
  // ── requiresWait 硬门控 ──
223
- const defStepsForCurrent = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
241
+ const defStepsForCurrent = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
224
242
  // def↔DB 一致性守卫(坑 execute-step-table-drift,2026-08-20 实证):runCommand 入口已按名
225
243
  // 重播种(ensureStageSteps),但同一命令进程内 plan.md 若再次被改(或绕过 runCommand 直调
226
244
  // completeStep 的路径),def 与 DB 步数错位会让 currentIdx 的门控/prompt 施加到错误的步骤上
@@ -312,6 +330,29 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
312
330
  }
313
331
  }
314
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
+ }
355
+
315
356
  // ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
316
357
  // 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
317
358
  // (stage.js noAI 分支,不写 step output);若 agent 对 noAI step 直接 --done,此前
@@ -336,11 +377,52 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
336
377
  await executeScanFinalize(cwd, platformOpts)
337
378
  } else if (_cliAction === 'planPostcheck') {
338
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 })
383
+ } else if (_cliAction === 'doctorRunDiagnostics') {
384
+ // 2026-09-09-doctor-noai:与 stage.js 分支同语义(诊断+渲染+落盘)
385
+ const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('../doctor-diagnostics.js')
386
+ const diag = await runDoctorDiagnostics({ cwd })
387
+ console.log(renderDoctorSummary(diag))
388
+ try { writeDoctorDiagnosis(diag, (platformOpts?.specRoot || join(cwd, '.sillyspec'))) } catch { /* 落盘 fail-soft */ }
339
389
  } else {
340
390
  throw new Error(`noAI 步骤 ${steps[currentIdx].name} 的未知 _cliAction: ${_cliAction}——请在 complete.js 注册对应分支`)
341
391
  }
342
392
  }
343
393
 
394
+ // ── 并发 --done 防护(坑 execute-concurrent-done-skips-next-wave,2026-09-08 实证)──
395
+ // 会话 B 先 --done 推进了 Wave N,会话 A 随后的 --done 会落到**新的当前步 = Wave N+1**,
396
+ // 用 Wave N 的摘要静默完成 Wave N+1(产物未落地却标完成)。CLI 短进程无法读心,两层防护:
397
+ // ① 醒目前置横幅(邻步 60s 内刚被完成 = 疑似并行会话刚推进)——操作者在看到「✅ Step N
398
+ // 完成:Wave X」与自己所做不符时可立即 --reopen,不静默;
399
+ // ② 写前重读校验(本命令读库→落盘窗口内的真实竞态):重读 DB,若本命令要完成的步骤
400
+ // 已被并行进程标 completed/skipped,拒绝覆盖式推进(防双写交错把状态机写花)。
401
+ if (stageName === 'execute' && currentIdx > 0) {
402
+ const prev = steps[currentIdx - 1]
403
+ const _prevDoneAgoSec = prev?.completedAt ? Math.round((Date.now() - new Date(prev.completedAt.replace(/(\d{4})\/(\d{2})\/(\d{2})/, '$1-$2-$3')).getTime()) / 1000) : null
404
+ if (_prevDoneAgoSec !== null && Number.isFinite(_prevDoneAgoSec) && _prevDoneAgoSec >= 0 && _prevDoneAgoSec <= 60) {
405
+ console.warn('')
406
+ console.warn(`⚠️ 并发防护:上一步「${prev.name}」${_prevDoneAgoSec}s 前刚被(可能另一会话的)--done 完成——本次将完成的是「${steps[currentIdx].name}」。`)
407
+ console.warn(` 若与你刚做完的 Wave 不符,立即停手:sillyspec run ${stageName} --reopen --from-step ${currentIdx + 1}${changeName ? ` --change ${changeName}` : ''} 退回本步,勿继续下一 Wave。`)
408
+ console.warn('')
409
+ }
410
+ }
411
+ let _dupCompletedByPeer = false
412
+ try {
413
+ const _fresh = pm.read(cwd, changeName)
414
+ const _freshSteps = _fresh?.stages?.[stageName]?.steps
415
+ if (Array.isArray(_freshSteps) && _freshSteps[currentIdx] && _freshSteps[currentIdx].name === steps[currentIdx].name
416
+ && (_freshSteps[currentIdx].status === 'completed' || _freshSteps[currentIdx].status === 'skipped')) {
417
+ _dupCompletedByPeer = true
418
+ }
419
+ } catch { /* 重读失败(平台模式瞬态等)不阻断——横幅防护仍在 */ }
420
+ if (_dupCompletedByPeer) {
421
+ console.error(`❌ 步骤「${steps[currentIdx].name}」刚被并行会话完成(本命令读库后被推进),拒绝重复推进——防把下一步误标完成。`)
422
+ console.error(` 排查:sillyspec progress show${changeName ? ` --change ${changeName}` : ''} 确认当前步;你若也完成了同一 Wave,无需再次 --done;若要完成的是下一步,重跑 --done(此时它会落在正确的当前步)。`)
423
+ process.exit(1)
424
+ }
425
+
344
426
  steps[currentIdx].status = 'completed'
345
427
  steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
346
428
  if (outputText) {
@@ -356,6 +438,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
356
438
  mkdirSync(artifactBase, { recursive: true })
357
439
  const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
358
440
  writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
441
+ // 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):artifacts/ 只写不收,
442
+ // 实证本仓 534 份累积。文件名时间戳在尾部、前缀是变更名(长变更晚收尾会乱序)→ 按
443
+ // mtime 排序(write-once 本地审计文件,mtime 可靠)。fail-open 静默。
444
+ try {
445
+ const { pruneTimestampedEntries } = await import('../runtime-hygiene.js')
446
+ pruneTimestampedEntries({ dir: artifactBase, keep: 100, orderBy: 'mtime' })
447
+ } catch { /* 卫生动作 fail-open */ }
359
448
  } else {
360
449
  steps[currentIdx].output = outputText
361
450
  }
@@ -483,6 +572,11 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
483
572
  }
484
573
  }
485
574
 
575
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
576
+ // `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
577
+ // currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
578
+ // (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
579
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
486
580
  stageData.status = 'completed'
487
581
  stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
488
582
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
@@ -606,7 +700,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
606
700
  }
607
701
  }
608
702
 
609
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
703
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
610
704
  console.log(`✅ Step ${currentIdx + 1}/${steps.length} 完成:${steps[currentIdx].name}\n`)
611
705
 
612
706
  // Workflow post_check(W6 Step6 抽至 complete-handlers.js handleWorkflowPostCheck)
@@ -1007,7 +1101,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
1007
1101
 
1008
1102
  // maxWaitRounds 硬上限:达到后拒绝继续 --wait
1009
1103
  const currentStep = stageData.steps[currentIdx]
1010
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1104
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1011
1105
  const stepDef = defSteps?.[currentIdx] || {}
1012
1106
  const maxWaitRounds = currentStep.maxWaitRounds ?? stepDef.maxWaitRounds
1013
1107
  const currentWaitRound = currentStep.waitRound || 0
@@ -1121,7 +1215,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
1121
1215
  } else {
1122
1216
  currentIdx = waitingSteps[0].idx
1123
1217
  }
1124
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1218
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1125
1219
  const currentStepDef = defSteps?.[currentIdx] || {}
1126
1220
  const currentStep = stageData.steps[currentIdx]
1127
1221
  const isRepeatableWait = currentStepDef.repeatableWait === true || currentStep.repeatableWait === true
@@ -1219,6 +1313,11 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
1219
1313
  if (nextPendingIdx === -1 && nextWaitingIdx === -1) {
1220
1314
  stageData.status = 'completed'
1221
1315
  stageData.completedAt = now
1316
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
1317
+ // `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
1318
+ // currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
1319
+ // (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
1320
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
1222
1321
  // persist _write 移到 completeStageGates 成功之后(task-03 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
1223
1322
  // 阶段完成收尾共享管线(含 execute worktree cleanup),消除 continueStep 完成分支绕过 gate 的 S2(task-01 抽出)。
1224
1323
  // gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(与 completeStep 同语义)。
@@ -1303,7 +1402,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
1303
1402
  process.exit(1)
1304
1403
  }
1305
1404
 
1306
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1405
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1307
1406
  const stepDef = defSteps ? defSteps[currentIdx] : null
1308
1407
  if (stepDef && !stepDef.optional) {
1309
1408
  console.error(`❌ 步骤 "${steps[currentIdx].name}" 不可跳过`)