sillyspec 3.31.0 → 3.32.1

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 (74) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  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 +10 -6
  5. package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
  6. package/.zcodeignore +66 -0
  7. package/AGENTS.md +16 -16
  8. package/assets/command-cards/flow.md +9 -3
  9. package/assets/command-cards/run-quick.md +4 -1
  10. package/package.json +2 -2
  11. package/src/agent-session-log.js +27 -5
  12. package/src/config-schema.js +21 -0
  13. package/src/datetime.js +51 -0
  14. package/src/db.js +10 -3
  15. package/src/decision-distill.js +21 -12
  16. package/src/design-facts.js +46 -6
  17. package/src/flow-draft.js +468 -95
  18. package/src/flow-parity.js +199 -5
  19. package/src/flow-review.js +136 -6
  20. package/src/flow.js +786 -145
  21. package/src/foreign-declared.js +75 -3
  22. package/src/fr-index.js +148 -162
  23. package/src/git-helper.js +16 -3
  24. package/src/hooks/worktree-guard.js +16 -5
  25. package/src/hunk-attribution.js +28 -5
  26. package/src/index.js +186 -19
  27. package/src/knowledge-classify.js +3 -1
  28. package/src/knowledge-digest.js +55 -26
  29. package/src/knowledge-match.js +135 -7
  30. package/src/knowledge-stats.js +105 -4
  31. package/src/knowledge-vector.js +160 -0
  32. package/src/machine-interface.js +90 -2
  33. package/src/module-impact.js +22 -1
  34. package/src/modules.js +9 -0
  35. package/src/progress/change-registry.js +20 -0
  36. package/src/progress/stage-machine.js +4 -7
  37. package/src/progress.js +8 -1
  38. package/src/quicklog.js +20 -0
  39. package/src/redomain.js +23 -7
  40. package/src/route-hindsight.js +276 -0
  41. package/src/run/archive-distill.js +3 -1
  42. package/src/run/command.js +65 -12
  43. package/src/run/complete-handlers.js +219 -5
  44. package/src/run/complete.js +99 -22
  45. package/src/run/gate-snapshot.js +15 -0
  46. package/src/run/gates.js +60 -4
  47. package/src/run/prompt.js +159 -31
  48. package/src/run/quick-audit.js +66 -7
  49. package/src/run/shared.js +6 -11
  50. package/src/run/stage.js +5 -1
  51. package/src/run/verify-quality-scan.js +84 -0
  52. package/src/scan-facts.js +2 -1
  53. package/src/scope-audit.js +138 -5
  54. package/src/sentinel-assertions.js +67 -16
  55. package/src/stage-contract-engine.js +4 -1
  56. package/src/stage-contract-spec.js +103 -4
  57. package/src/stage-contract.js +55 -7
  58. package/src/stages/brainstorm.js +27 -14
  59. package/src/stages/knowledge.js +4 -3
  60. package/src/stages/plan-postcheck.js +98 -3
  61. package/src/stages/plan.js +22 -2
  62. package/src/stages/quick.js +1 -1
  63. package/src/stages/verify.js +2 -1
  64. package/src/task-review.js +1 -1
  65. package/src/task-tick.js +121 -0
  66. package/src/taskcard-frontmatter.js +31 -0
  67. package/src/ui-visual.js +43 -8
  68. package/src/verify-postcheck.js +518 -38
  69. package/src/verify-probes.js +216 -5
  70. package/src/watcher.js +1067 -1031
  71. package/src/worktree-apply.js +9 -1
  72. package/src/worktree.js +21 -6
  73. package/templates/agents-instruction.md +15 -15
  74. package/templates/prompts/taskcard-rules.md +1 -0
@@ -29,7 +29,7 @@ import { resolveQuickLinkedChanges } from './quick-audit.js'
29
29
  import { outputStep, collectStageWaitHistory } from './prompt.js'
30
30
  import { completeStep, completeStepBurst, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
31
31
  import { runStage } from './stage.js'
32
- import { sanitizeDesc } from '../quicklog.js'
32
+ import { sanitizeDesc, deriveChangeTitle } from '../quicklog.js'
33
33
  import { ProgressManager, resolveSessionIdentity } from '../progress.js'
34
34
  import { validateChangeExists, checkTransition } from '../stage-contract.js'
35
35
  import { READONLY_AUXILIARY_STAGES } from '../constants.js'
@@ -215,6 +215,22 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
215
215
  * run <stage> --help/-h 的用法帮助(runCommand 内 --help 短路调用)。
216
216
  * 只打印不落盘:帮助查询不该有副作用(建会话/写 QUICKLOG)。
217
217
  */
218
+ /**
219
+ * 只读空态引导文案(2026-10-05-status-empty-guide 首创;2026-10-06-status-multi-active-list
220
+ * 抽出单一源——库不在场前置门与 read 后空分支共用一份,防两处文案漂移)。静态文案——引用
221
+ * 通用命令形态而非本仓状态(空态定义即无数据可读)。可照抄实例(2026-10-05-input-teach-copyable):
222
+ * 引号内换行合法,extractSuccessCriteria 行级 trim 容忍缩进,带缩进照抄亦过门。
223
+ */
224
+ function emitReadonlyEmptyGuide() {
225
+ console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
226
+ console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
227
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
228
+ console.log('')
229
+ console.log(' 成功标准:')
230
+ console.log(' - <可验证标准>"')
231
+ console.log(' → 需求不明可 sillyspec run brainstorm --change <名> 先探索')
232
+ }
233
+
218
234
  function printStageUsage(stageName) {
219
235
  const stage = stageRegistry[stageName]
220
236
  console.log(`
@@ -833,7 +849,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
833
849
  quickSessionId = recent
834
850
  quickFallbackUsed = true
835
851
  } else {
836
- console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更:sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"')
852
+ console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
853
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
854
+ console.log('')
855
+ console.log(' 成功标准:')
856
+ console.log(' - <可验证标准>"')
837
857
  process.exit(0)
838
858
  }
839
859
  } else {
@@ -1111,9 +1131,23 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1111
1131
  }
1112
1132
  }
1113
1133
 
1134
+ // 只读辅助阶段的写意图 flag 清单(下方两处只读短路共用:库不在场前置门 + read 后短路)
1135
+ const READONLY_WRITE_ACTIONS = ['--cleanup-remnant', '--align-execute-progress', '--done', '--skip', '--reset', '--reopen', '--wait', '--continue']
1136
+
1137
+ // 只读前置门(2026-10-06-status-multi-active-list):库文件不在场时不得进入 pm.read()——
1138
+ // read() 无显式 --change 先走 listChanges→_ensureDB,会凭空新建空库(只读承诺破防,实测);
1139
+ // 空库撞残留 schema-version 戳无表可查,首个 SELECT 直接崩(no such table: changes)。
1140
+ // 库不在场=字面意义的零进度:与零活跃同款空态引导 + exit 0,零落盘。
1141
+ if (READONLY_AUXILIARY_STAGES.includes(stageName) && !flags.some(f => READONLY_WRITE_ACTIONS.includes(f))
1142
+ && !existsSync(pm._runtimePath(cwd, 'sillyspec.db'))) {
1143
+ emitReadonlyEmptyGuide()
1144
+ process.exit(0)
1145
+ }
1146
+
1114
1147
  let progress = pm.read(cwd, changeName)
1115
1148
 
1116
- // task-03 (D-005@v2 / FR-04): READONLY_AUXILIARY_STAGES(status/doctor)查询只读短路。
1149
+ // task-03 (D-005@v2 / FR-04): READONLY_AUXILIARY_STAGES(status;doctor 已于 2026-09-09-doctor-noai
1150
+ // 移出——阶段形态走状态机,顶层 doctor 只读语义由 index.js case 头部分流保真)查询只读短路。
1117
1151
  // 无显式写 flag 时在 registerChange / ensureStageSteps 之前拦截,零副作用:
1118
1152
  // - 目标 progress 不存在 → 提示「只读查询不建变更」exit 0(不 initChange、不建 default 行,治 8b)
1119
1153
  // - 目标 progress 存在 → 只读展示,不 seed steps、不刷新 lastActive、不 registerChange
@@ -1121,10 +1155,25 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1121
1155
  // 显式写意图不短路:doctor 写操作 flag(--cleanup-remnant / --align-execute-progress)与
1122
1156
  // 步骤动作 flag(--done/--skip/--reset/--reopen/--wait/--continue)走原写路径;--status 本身
1123
1157
  // 是只读查询,纳入短路。--fix 是 worktree doctor 的 flag 不在此列(design.md Phase 4)。
1124
- const READONLY_WRITE_ACTIONS = ['--cleanup-remnant', '--align-execute-progress', '--done', '--skip', '--reset', '--reopen', '--wait', '--continue']
1125
1158
  if (READONLY_AUXILIARY_STAGES.includes(stageName) && !flags.some(f => READONLY_WRITE_ACTIONS.includes(f))) {
1126
1159
  if (!progress) {
1127
- console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
1160
+ // 多活跃分流(2026-10-06-status-multi-active-list):read() 无显式 --change 在 ≥2 活跃
1161
+ // 时同样返回 null——「有数据但需要点名」被空态文案一律误报成「无数据」(本仓实测
1162
+ // 10 行 active、2 个目录待收口却被告知未找到)。前置门保证走到这库必在场,listChanges
1163
+ // 纯读;显式 --change 未命中仍走空态引导(点名失败与未点名歧义是两件事)。
1164
+ let activeChanges = []
1165
+ try { activeChanges = pm.listChanges(cwd) || [] } catch { activeChanges = [] }
1166
+ if (!changeName && activeChanges.length >= 2) {
1167
+ console.log(`ℹ️ 进度数据存在,但有 ${activeChanges.length} 个活跃变更(未指定 --change,无法确定展示哪个):`)
1168
+ for (const name of activeChanges) {
1169
+ // DB 行与目录是两个真相层:目录缺失的幽灵行如实标注(清幽灵走既有 doctor --cleanup-remnant)
1170
+ console.log(` - ${name}${existsSync(pm._changePath(cwd, name)) ? '' : '(本地无目录)'}`)
1171
+ }
1172
+ console.log(` → 指定查看其一:sillyspec status --change ${activeChanges[0]}`)
1173
+ console.log(' → 恢复面:sillyspec flow status --change <名>')
1174
+ process.exit(0)
1175
+ }
1176
+ emitReadonlyEmptyGuide()
1128
1177
  process.exit(0)
1129
1178
  }
1130
1179
  // 只读展示路径:复用阶段定义渲染当前步骤 prompt(outputStep 对 status/doctor 纯只读),
@@ -1225,10 +1274,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1225
1274
  // 如果指定了变更名或有变更目录,自动初始化变更的 progress
1226
1275
  const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
1227
1276
  if (autoChange) {
1228
- // 创建时即写 title(与 name/KEY 同时落 db):--input 需求描述 sanitizeDesc 优先,无则用 name 兜底。
1277
+ // 创建时即写 title(与 name/KEY 同时落 db):--input 首行推导 deriveChangeTitle(≤50 字,
1278
+ // 用户需求 2026-09-29)优先,长输入兜底 sanitizeDesc 压行,无则用 name 兜底。
1229
1279
  // proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
1230
1280
  // complete.js 通用完成路径 deriveTitleFromLinkedChange 刷新为真实 # 标题(与 quick 启动 title 同源)。
1231
- progress = pm.initChange(cwd, autoChange, { title: inputText ? sanitizeDesc(inputText) : autoChange })
1281
+ progress = pm.initChange(cwd, autoChange, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoChange })
1232
1282
  } else if (isAuxiliary) {
1233
1283
  let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
1234
1284
  // archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
@@ -1255,7 +1305,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1255
1305
  changeName = autoName
1256
1306
  if (!progress) {
1257
1307
  // auxiliary 创建也写 title(--input 优先 / name 兜底),同上完整流程语义。
1258
- progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
1308
+ progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
1259
1309
  // initChange 可能因 project 表为空返回 null
1260
1310
  if (!progress) {
1261
1311
  progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
@@ -1287,7 +1337,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1287
1337
  console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
1288
1338
  console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
1289
1339
  // brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
1290
- progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
1340
+ progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
1291
1341
  changeName = autoName
1292
1342
  } else if (stageName === 'auto') {
1293
1343
  // auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
@@ -1303,7 +1353,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1303
1353
  const date = new Date().toISOString().slice(0, 10)
1304
1354
  const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
1305
1355
  console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
1306
- progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
1356
+ progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
1307
1357
  changeName = autoName
1308
1358
  } else {
1309
1359
  console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
@@ -2010,8 +2060,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
2010
2060
  const classification = classifyChange({ description: inputText || '', explicitMode, localConfig })
2011
2061
  if (classification.mode === 'quick') {
2012
2062
  console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
2013
- console.log(` 小变更请走轻量变更(quick 通道已退役,v3.31.0 起拒绝新会话):`)
2014
- console.log(` sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"`)
2063
+ console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
2064
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
2065
+ console.log('')
2066
+ console.log(' 成功标准:')
2067
+ console.log(' - <可验证标准>"')
2015
2068
  return
2016
2069
  }
2017
2070
  console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
@@ -410,6 +410,60 @@ export function extractPendingDocSyncRows(content) {
410
410
  return rows
411
411
  }
412
412
 
413
+ /**
414
+ * module-impact.md pending 死信一键回填(2026-10-07-flow-friction-batch3 task-04):verify 硬拦
415
+ * 「更新结果」表 pending/待办行后,agent 实测写一次性 node 脚本批量回填——CLI 化。语义对齐
416
+ * 文件内规则行「确定不同步的行改 skipped 并在操作列写明原因」:状态列 pending/待办→skipped,
417
+ * --reason 追加进操作列(`——skipped:<reason>`);done/其余状态行与表外内容逐字不动;幂等
418
+ * (无 pending 行时零写盘)。与 extractPendingDocSyncRows 同一节段解析口径(不二算规则集)。
419
+ * @param {string} mdPath module-impact.md 路径
420
+ * @param {{ reason?: string }} opts reason 追加进操作列的一句话原因(缺省只翻状态)
421
+ * @returns {{ filled: number, ok: boolean, error?: string }} filled=回填行数;ok=false 时 error 带原因
422
+ */
423
+ export function fillModuleImpactSkipped(mdPath, { reason = '' } = {}) {
424
+ let content
425
+ try { content = readFileSync(mdPath, 'utf8') } catch (e) {
426
+ return { filled: 0, ok: false, error: `module-impact.md 不可读:${mdPath}(${e && e.message ? e.message : e})` }
427
+ }
428
+ const normalized = content.replace(/\r\n/g, '\n')
429
+ const sectionStart = normalized.search(/^#{2,3}\s*更新结果\s*$/m)
430
+ if (sectionStart === -1) return { filled: 0, ok: false, error: 'module-impact.md 无「更新结果」小节——无可回填行(先核对文件是否为骨架未填形态)' }
431
+ const titleMatch = normalized.slice(sectionStart).match(/^#{2,3}\s*更新结果\s*$/m)
432
+ const bodyStart = sectionStart + titleMatch[0].length
433
+ const afterTitle = normalized.slice(bodyStart)
434
+ const nextSection = afterTitle.search(/^#{1,3}\s/m)
435
+ const section = nextSection === -1 ? afterTitle : afterTitle.slice(0, nextSection)
436
+ const sectionEnd = nextSection === -1 ? normalized.length : bodyStart + nextSection
437
+ const PENDING_STATUSES = new Set(['pending', '待办', '未同步', 'not-done', 'todo'])
438
+ let filled = 0
439
+ const newSection = section.split('\n').map((line) => {
440
+ const trimmed = line.trim()
441
+ if (!trimmed.startsWith('|')) return line
442
+ const cells = trimmed.split('|').map(c => c.trim())
443
+ const lastCell = cells[cells.length - 1]
444
+ if (lastCell === '' && cells.length >= 2) cells.pop()
445
+ const statusCell = cells[cells.length - 1]
446
+ if (!statusCell || !PENDING_STATUSES.has(statusCell.toLowerCase())) return line
447
+ filled++
448
+ // 结构 | 目标 | 操作 | 状态 | → cells ['', 目标, 操作, 状态];操作列 = 倒数第二格
449
+ if (cells.length >= 3 && reason) {
450
+ cells[cells.length - 2] = `${cells[cells.length - 2]}——skipped:${reason}`
451
+ }
452
+ cells[cells.length - 1] = 'skipped'
453
+ // 重拼保留「| a | b | c |」空格形态(cells[0] 是行首 | 产生的空串);行首缩进对齐原行
454
+ const indent = line.slice(0, line.indexOf('|'))
455
+ return `${indent}| ${cells.slice(1).join(' | ')} |`
456
+ }).join('\n')
457
+ if (filled === 0) return { filled: 0, ok: true }
458
+ const next = normalized.slice(0, bodyStart) + newSection + normalized.slice(sectionEnd)
459
+ try {
460
+ writeFileSync(mdPath, next, 'utf8')
461
+ return { filled, ok: true }
462
+ } catch (e) {
463
+ return { filled: 0, ok: false, error: `回填写盘失败:${e && e.message ? e.message : e}` }
464
+ }
465
+ }
466
+
413
467
  /**
414
468
  * 提取 module-impact.md「更新结果」表中的已完成(done)行声明的目标文档路径(债单 D-4 窄口径)。
415
469
  *
@@ -508,6 +562,47 @@ export function resolveArchiveDocAddPaths(cwd, specBase, tokens) {
508
562
  return [...out]
509
563
  }
510
564
 
565
+ /**
566
+ * 归档源侧移动补暂存(坑 archive-stage-claim,2026-10-06-module-map-list-leak 收口实测发现)。
567
+ *
568
+ * safeGit 契约是返回 {value, error} 不抛错——此前调用方忽略返回值无条件打成功提示,add 失败
569
+ * (index.lock 竞态等)时「已补暂存 N 项」假成功、暂存面实际缺项(实测声称 1 项暂存面为空,
570
+ * 靠核对暂存面纪律兜住后手工补提交)。本助手逐批 add 并校验 error,失败明细返回给调用方
571
+ * 择路打印(fail-soft:不抛错不阻断归档链,幂等 add 重跑同值)。
572
+ *
573
+ * @param {{ cwd: string, paths: string[] }} opts paths 已按本变更目录名过滤的精确 pathspec
574
+ * @returns {{ ok: boolean, count: number, failures: Array<{ path: string, error: string }> }}
575
+ */
576
+ export function stageArchiveSourceSideMoves({ cwd, paths }) {
577
+ const failures = []
578
+ let count = 0
579
+ for (const batch of chunkPaths(paths)) {
580
+ const r = safeGit(cwd, ['add', '--', ...batch])
581
+ if (r && r.error) {
582
+ for (const p of batch) failures.push({ path: p, error: r.error })
583
+ } else {
584
+ count += batch.length
585
+ }
586
+ }
587
+ return { ok: failures.length === 0, count, failures }
588
+ }
589
+
590
+ /**
591
+ * 补暂存结果文案(成功=既有提示逐字不变;失败=⚠️ 告警含失败路径与 error 首行 + 手工兜底指引;
592
+ * 空清单零文案)。独立导出供测试断言两半(返回值与文案),打印决策归调用方。
593
+ * @param {{ ok: boolean, count: number, failures: Array<{ path: string, error: string }> }} r
594
+ * @returns {string}
595
+ */
596
+ export function renderStageSourceSideMovesMessage(r) {
597
+ if (!r || r.count === 0 && (!r.failures || r.failures.length === 0)) return ''
598
+ if (r.ok) return `🧾 已补暂存本变更归档的源侧移动(${r.count} 项,归档成单次原子提交)`
599
+ const lines = [`⚠️ 补暂存本变更归档的源侧移动失败(${r.failures.length} 个路径,成功 ${r.count} 项)——暂存面可能缺项,归档提交前请核对 git diff --cached --name-only:`]
600
+ for (const f of r.failures.slice(0, 10)) lines.push(` - ${f.path}(${f.error})`)
601
+ if (r.failures.length > 10) lines.push(` … 共 ${r.failures.length} 个`)
602
+ lines.push(` 手工兜底:git add -- <上述路径> 后重跑收口命令(归档子步幂等续)`)
603
+ return lines.join('\n')
604
+ }
605
+
511
606
  /**
512
607
  * 归档收尾的窄化 git add(ql-20260915-001 修复④,坑 archive-git-add-sweeps-parallel-docs)。
513
608
  *
@@ -731,6 +826,60 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
731
826
  *
732
827
  * @returns {Promise<void>} 失败面沿链内既有 process.exit 语义(纯搬运不改)。
733
828
  */
829
+ /**
830
+ * 归档提交 pathspec 构成(2026-10-06-archive-cmd-race-and-brief:共享暂存区竞态加固)。
831
+ *
832
+ * 背景:旧实现照抄 `git diff --cached --name-status` 单元格——多 agent 共享仓下,兄弟会话/
833
+ * 后台同步可在「打印→执行」窗口瞬改暂存区(实测 flow-status-json 收尾:建议命令含转瞬即逝
834
+ * 的源侧 A 条目,照抄执行报 pathspec did not match;单会话镜像复盘命令干净=竞态即现即逝)。
835
+ *
836
+ * 构成按稳定性分级:
837
+ * - src 侧(changes/<名>/ 非归档):仅取 HEAD 树在册路径——HEAD 不可变,不受暂存区竞态
838
+ * 影响;rename src / 删除 D 恒在册,转瞬即逝的源侧 A 条目恒不在册(天然滤幽灵)。
839
+ * - dst 侧:本变更专属归档目录单条 pathspec(archive/<名>/ 与窄化 add 同一专属前提),
840
+ * 目录在场即匹配,条目级抖动无感。
841
+ * - knowledge/docs 共享面:HEAD 在册(M 形态)或工作区在场(蒸馏新产 A 形态)才入列。
842
+ * - 其余丢弃并回报(dropped),由调用方显式提示。
843
+ *
844
+ * 纯函数(git 读数由调用方喂入),单测直接钉构成口径。
845
+ * @param {{changeName:string, stagedNameStatus:string, headTreeFiles:string, existsFn?:(p:string)=>boolean}} p
846
+ * @returns {{pathspecs:string[], dropped:string[]}}
847
+ */
848
+ export function resolveArchiveCommitPathspecs({ changeName, stagedNameStatus, headTreeFiles, existsFn }) {
849
+ const changesPrefix = '.sillyspec/changes/'
850
+ // 与旧实现同源的单元格收集(--name-status:R 行 src<TAB>dst 两格,A/M 单格)
851
+ const mineCells = new Set()
852
+ for (const line of String(stagedNameStatus || '').split('\n')) {
853
+ if (!line.trim()) continue
854
+ for (const cell of line.split('\t').slice(1)) {
855
+ const p = cell.trim().replace(/^"|"$/g, '')
856
+ if (!p) continue
857
+ if (p.startsWith(changesPrefix + changeName + '/')
858
+ || p.startsWith(changesPrefix + 'archive/' + changeName + '/')
859
+ || p.startsWith('.sillyspec/knowledge/')
860
+ || p.startsWith('.sillyspec/docs/')) mineCells.add(p)
861
+ }
862
+ }
863
+ const headSet = new Set(String(headTreeFiles || '').split('\n').map((s) => s.trim()).filter(Boolean))
864
+ const srcPaths = []
865
+ let archiveHit = false
866
+ const sharedPaths = []
867
+ const dropped = []
868
+ for (const p of mineCells) {
869
+ if (p.startsWith(changesPrefix + 'archive/' + changeName + '/')) {
870
+ archiveHit = true
871
+ } else if (p.startsWith(changesPrefix + changeName + '/')) {
872
+ if (headSet.has(p)) srcPaths.push(p)
873
+ else dropped.push(p)
874
+ } else {
875
+ if (headSet.has(p) || (typeof existsFn === 'function' && existsFn(p))) sharedPaths.push(p)
876
+ else dropped.push(p)
877
+ }
878
+ }
879
+ const pathspecs = [...(archiveHit ? [`${changesPrefix}archive/${changeName}/`] : []), ...srcPaths, ...sharedPaths]
880
+ return { pathspecs, dropped }
881
+ }
882
+
734
883
  export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, destDir, skipApply = false, skipPlanCheck = false, deliverableFiles = null, docsIncrement = null } = {}) {
735
884
  const archiveDir = dirname(destDir) // 抽取补:原闭包变量(changes/archive),从 destDir 推导等价
736
885
  // ── 模块文档认领 advisory(资产三小件③,2026-09-23)──
@@ -858,9 +1007,16 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
858
1007
  // pathspec 把并行会话同目录未提交文件夹带进共享暂存区):改为窄化——changes 侧只 add 本变更
859
1008
  // 归档目录 archive/<destName>/,docs 侧按 module-impact「更新结果」done 行精确文件集(提取
860
1009
  // 失败回退目录级 + 前置 warning,见 archiveNarrowedGitAdd)。
1010
+ // destName 推导(2026-10-05-flow-tail-polish 根因修复):本链签名无 destName——此前下方
1011
+ // 调用引用未声明标识符抛 ReferenceError 被空 catch 静默吞,archiveNarrowedGitAdd 在 flow
1012
+ // done 路径从未生效(归档目录/knowledge/docs 自动暂存全失效,靠 agent 手工兜底——实测
1013
+ // porcelain 实证)。basename(destDir) 与调用方 destDir 恒一致(flow.js 与 run archive 同构)。
1014
+ const destName = basename(destDir)
861
1015
  try {
862
1016
  archiveNarrowedGitAdd({ cwd, specBase, destDir, destName })
863
- } catch {}
1017
+ } catch (e) {
1018
+ console.warn(`⚠️ 归档窄化 git add 异常(不阻断归档,交付面靠下方探测块兜底/agent git status 核对): ${(e && e.message) || e}`)
1019
+ }
864
1020
 
865
1021
  // ── 他者半归档残留探测(坑 archive-other-residual-rename,2026-08-21 实证)──
866
1022
  // 并行变更的手动归档把 R 残留(源目录 rename 目标行)留在暂存区;本变更归档提交时
@@ -875,6 +1031,10 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
875
1031
  const changesPrefix = '.sillyspec/changes/'
876
1032
  const minePaths = []
877
1033
  const othersResidual = []
1034
+ // 兜底信号(2026-10-05-flow-tail-polish):正常路径 archiveNarrowedGitAdd 已暂存归档目录
1035
+ // 与 knowledge——这两个信号只在窄化 add 失败降级时非空/置位
1036
+ let untrackedArchiveHit = false
1037
+ const knowledgePending = []
878
1038
  for (const line of raw.split('\n')) {
879
1039
  if (!line || line.length < 4) continue
880
1040
  const x = line[0], y = line[1]
@@ -894,18 +1054,72 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
894
1054
  || p.startsWith(changesPrefix + 'archive/' + changeName + '/'))) {
895
1055
  minePaths.push(p)
896
1056
  }
1057
+ } else if (x === '?' && y === '?') {
1058
+ // 未跟踪兜底:归档新目录(目录聚合/逐文件两形态都收,archive/<me>/ 专属本变更零共享)
1059
+ if (dst === `${changesPrefix}archive/${changeName}/` || dst.startsWith(`${changesPrefix}archive/${changeName}/`)) {
1060
+ untrackedArchiveHit = true
1061
+ } else if (dst.startsWith('.sillyspec/knowledge/')) {
1062
+ knowledgePending.push(dst)
1063
+ }
1064
+ } else if (x === ' ' && y === 'M' && dst.startsWith('.sillyspec/knowledge/')) {
1065
+ // knowledge 未暂存修改(同兜底语义)
1066
+ knowledgePending.push(dst)
897
1067
  }
898
1068
  }
899
1069
  if (minePaths.length > 0) {
900
1070
  // ql-20260915-001 修复④:补暂存源侧移动改精确 pathspec(minePaths 已按本变更目录名
901
1071
  // 过滤,chunkPaths 分批防 Windows argv 上限)——原 add -A -- .sillyspec/changes/ 目录级
902
1072
  // 会顺带扫入并行会话在 changes/ 下的未提交文件(坑 archive-git-add-sweeps-parallel-docs
903
- // 同坑不同点;git add -- <已删路径> 即暂存删除,语义等价 -A 限本变更面)
904
- for (const batch of chunkPaths(minePaths)) {
905
- safeGit(cwd, ['add', '--', ...batch])
1073
+ // 同坑不同点;git add -- <已删路径> 即暂存删除,语义等价 -A 限本变更面)。
1074
+ // 坑 archive-stage-claim(2026-10-06-module-map-list-leak 收口实测):safeGit 不抛错返回
1075
+ // {value,error}——此前忽略返回值无条件打成功提示,add 失败(index.lock 竞态等)时假成功。
1076
+ const staged = stageArchiveSourceSideMoves({ cwd, paths: minePaths })
1077
+ const msg = renderStageSourceSideMovesMessage(staged)
1078
+ if (msg) (staged.ok ? console.log : console.warn)(msg)
1079
+ }
1080
+ if (untrackedArchiveHit) {
1081
+ // 文件级枚举补暂存(守 AGENTS.md 规则 11 不用目录级 add;实际仅窄化 add 降级时可达)
1082
+ try {
1083
+ const archFiles = []
1084
+ const walk = (d) => { for (const e of readdirSync(d, { withFileTypes: true })) { const p = join(d, e.name); if (e.isDirectory()) walk(p); else archFiles.push(p) } }
1085
+ walk(destDir)
1086
+ const rel = archFiles.map((p) => relative(cwd, p).replace(/\\/g, '/'))
1087
+ for (const batch of chunkPaths(rel)) safeGit(cwd, ['add', '--', ...batch])
1088
+ console.log(`🧾 已补暂存未跟踪的归档目录文件(${rel.length} 项,archive/${changeName}/ 专属本变更——窄化 add 降级兜底)`)
1089
+ } catch (e) {
1090
+ console.warn(`⚠️ 归档目录兜底补暂存失败(请手工 git add -- .sillyspec/changes/archive/${changeName}/ 下文件): ${(e && e.message) || e}`)
906
1091
  }
907
- console.log(`🧾 已补暂存本变更归档的源侧移动(${minePaths.length} 项,归档成单次原子提交)`)
908
1092
  }
1093
+ if (knowledgePending.length > 0) {
1094
+ console.log(`📎 归档提交待办(knowledge 蒸馏产物未暂存——核对归属后随归档提交一并带上):`)
1095
+ console.log(` git add -- ${knowledgePending.join(' ')}`)
1096
+ }
1097
+ // 归档提交一笔到位提示(2026-10-06-litest-p1-fixes 实测 b112d738/2c8fd25c:git commit
1098
+ // -- <pathspec> 是部分提交——rename 条目 pathspec 只匹配 archive/ 侧时提交树里源文件
1099
+ // 不会被删(D 留暂存区,源侧得再来一笔)。补暂存只保证暂存面完整,提交 pathspec 仍由
1100
+ // agent 掌握)。聚合本变更归档面全量 staged 路径打印可直接执行的一笔到位 commit 命令。
1101
+ // 2026-10-06-archive-cmd-race-and-brief 竞态加固:真仓实测(flow-status-json 收尾)照抄执行报
1102
+ // pathspec did not match——共享仓多会话/后台同步可在「打印→执行」窗口瞬改暂存区,单会话镜像
1103
+ // 复盘命令干净(竞态即现即逝)。照抄暂存面不可靠,pathspec 构成按稳定性分级(见
1104
+ // resolveArchiveCommitPathspecs JSDoc);无 staged 面不打印(幂等重入)。
1105
+ try {
1106
+ const stagedRaw = gitQuiet(cwd, ['diff', '--cached', '--name-status'])
1107
+ const headTreeRaw = gitQuiet(cwd, ['ls-tree', '-r', '--name-only', 'HEAD'])
1108
+ const { pathspecs, dropped } = resolveArchiveCommitPathspecs({
1109
+ changeName,
1110
+ stagedNameStatus: String(stagedRaw || ''),
1111
+ headTreeFiles: String(headTreeRaw || ''),
1112
+ existsFn: (p) => { try { return existsSync(join(cwd, p)) } catch { return false } },
1113
+ })
1114
+ if (pathspecs.length > 0) {
1115
+ console.log(`🧾 归档提交一笔到位(src 锚 HEAD 树/归档侧目录 pathspec/共享面按在场——共享暂存区竞态加固):`)
1116
+ console.log(` git commit -m "chore(archive): ${changeName} 归档留档" -- ${pathspecs.join(' ')}`)
1117
+ if (dropped.length > 0) {
1118
+ console.log(` ⚠️ 丢弃 ${dropped.length} 个瞬时暂存条目(HEAD 不在册且工作区不在场——并行会话动了共享暂存区,未入命令):${dropped.join('、')}`)
1119
+ }
1120
+ console.log(` (若执行报 pathspec 不匹配:共享仓暂存区在打印→执行间又被并行会话改动——git diff --cached --name-only 全量重核,以实际面替换 pathspec 后再提交)`)
1121
+ }
1122
+ } catch { /* 提示失败不阻断归档(advisory) */ }
909
1123
  if (othersResidual.length > 0) {
910
1124
  const owners = [...new Set(othersResidual)].filter(Boolean)
911
1125
  console.warn(`⚠️ 检测到「他者半归档」残留(暂存区存在其他变更的 rename 记录):${owners.join('、')}`)
@@ -18,9 +18,9 @@
18
18
  */
19
19
  import { join, isAbsolute, resolve } from 'node:path'
20
20
  import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
21
- import { createHash } from 'node:crypto'
22
21
  import { writeAtomicSync } from '../fs-atomic.js'
23
22
  import { gitQuiet } from '../git-helper.js'
23
+ import { writeCloseTraceArtifacts, projectTraceFaceRows } from '../flow-parity.js'
24
24
  import { withFileLock } from '../quicklog.js'
25
25
  import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer } from './shared.js'
26
26
  import { isExplicitReviewWrite, collectWorktreeChangedFiles } from '../task-review.js'
@@ -451,6 +451,70 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
451
451
  }
452
452
  }
453
453
 
454
+ // ── 方案步 --done 知识检索命中回显(FR-03/D-004,2026-09-28-unclear-req-to-brainstorm)──
455
+ // 设计时点此前零检索面(自动注入只锚变更入口)——方案步 --done 对 --output 与 decisions.md
456
+ // 当前条目(D-xxx 标题+question 拼串;执行期裁决 D-007:条目无时间戳,「自上次 --done 新增」
457
+ // 无机械锚点,取全部条目为超集)跑既有检索匹配器 matchKnowledge(复用不另写);命中 → warn
458
+ // 回显(decisionHits rejected 优先排序为匹配器既有行为)+ evidence 回应提示;v1 只 warn 不
459
+ // 阻断(D-004 故障面自留退路)。逃生阀 local.yaml commands.knowledge-gate: false/off 关回显
460
+ // (缺省开,config-schema 已登记);无命中输出与现状一致。fail-open:检索异常静默放行。
461
+ if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('提出 2-3 种方案') && changeName) {
462
+ try {
463
+ let _kgOn = true
464
+ try {
465
+ const _ly = readFileSync(join(specBase, 'local.yaml'), 'utf8')
466
+ const _kgm = _ly.match(/^\s*knowledge-gate\s*:\s*(true|false|on|off)\s*$/m)
467
+ if (_kgm && (_kgm[1] === 'false' || _kgm[1] === 'off')) _kgOn = false
468
+ } catch { /* 无 local.yaml = 缺省开 */ }
469
+ if (_kgOn) {
470
+ let _kgQuery = String(outputText || '')
471
+ let _dm = ''
472
+ try {
473
+ _dm = readFileSync(join(specBase, 'changes', changeName, 'decisions.md'), 'utf8')
474
+ const _parts = []
475
+ for (const m of _dm.matchAll(/^##\s+(D-\d+@v\d+):\s*(.+)$/gm)) _parts.push(`${m[1]} ${m[2]}`)
476
+ for (const m of _dm.matchAll(/^- question:\s*(.+)$/gm)) _parts.push(m[1])
477
+ if (_parts.length > 0) _kgQuery += '\n' + _parts.join('\n')
478
+ } catch { /* 无 decisions.md = 仅 --output */ }
479
+ _kgQuery = _kgQuery.slice(0, 4000)
480
+ if (_kgQuery.trim()) {
481
+ const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
482
+ const km = await matchKnowledgeHybrid(join(specBase, 'knowledge'), _kgQuery, { cwd })
483
+ // 零分不弹(2026-09-28-knowledge-gate-denoise):score 零且非死路的 rejected 与查询零主题
484
+ // 重叠——空标题条目靠状态蹭进回显是行为实测三例的噪音源;死路条目不受限(防复潮先验)。
485
+ const _hits = (km.decisionHits || []).filter((h) => h.deathPath || (h.status === 'rejected' && h.score > 0))
486
+ // 已回应不重弹:decisions.md 正文已含「命中 id+域文件名」共现(小白鼠回应形态
487
+ // 「unmapped.md D-001@v1」)→ 视为已甄别过,静默;同轮查询串仍含其词面属预期。
488
+ const _answered = _dm
489
+ ? (h) => {
490
+ const _dom = String(h.file || '').replace(/^decisions\//, '').replace(/\.md$/, '')
491
+ return _dm.includes(h.id) && _dm.includes(_dom)
492
+ }
493
+ : () => false
494
+ const _echoHits = _hits.filter((h) => !_answered(h))
495
+ const _entries = km.matched ? (km.entries || []) : []
496
+ if (_echoHits.length > 0 || _entries.length > 0) {
497
+ const { deathPathNote } = await import('../knowledge-match.js')
498
+ console.warn(`\n⚠️ [knowledge-gate] 方案/决策知识命中(设计时点防复潮提示,warn 不阻断):`)
499
+ for (const h of _echoHits.slice(0, 5)) {
500
+ const _note = h.status === 'rejected'
501
+ ? ` 否决理由:${h.reason || '(未记录)'}`
502
+ : h.deathPath ? ` ⚰️死路注记:${deathPathNote(h.reason)}` : ''
503
+ console.warn(` - ${h.id} ${h.title}(${h.file})status=${h.status || '?'}${_note}`)
504
+ }
505
+ for (const e of _entries.slice(0, 3)) {
506
+ const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
507
+ console.warn(` - 知识条目 ${_base}${e.display ? `(${e.display})` : ''}`)
508
+ }
509
+ console.warn(` 须在对应决策的 evidence 回应或说明不复潮(v1 提示级;local.yaml commands.knowledge-gate: false 可关本回显)`)
510
+ }
511
+ }
512
+ }
513
+ } catch (_kgEx) {
514
+ console.warn(` ⚠️ knowledge-gate 检索自身异常,fail-open 放行(不误拦本次完成):${_kgEx && _kgEx.message ? _kgEx.message : _kgEx}`)
515
+ }
516
+ }
517
+
454
518
  // ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
455
519
  // 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
456
520
  // (stage.js noAI 分支,不写 step output);若 agent 对 noAI step 直接 --done,此前
@@ -898,15 +962,17 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
898
962
  // verify 的"验证通过"提示延后到下方 validator 通过后才打印,
899
963
  // 避免校验失败(FAIL / 缺 verify-result.md)时仍声称"验证通过可以归档"。
900
964
  } else if (stageName === 'brainstorm') {
901
- // brainstorm 下一步按 design.md 的 scale 分叉(与末步 prompt 的 large→plan / small→quick 对齐),
965
+ // brainstorm 下一步按 design.md 的 scale 分叉(2026-09-29-brainstorm-exit-thin-default:large→plan;small/未标→flow start 收编),
902
966
  // 不走下方 _getNextSuggestion —— 后者按全局状态机「第一个未完成且上游就绪的阶段」推荐,
903
967
  // 当 scan 未完成时会误推 scan(回头路),与 brainstorm 已完成、应进入 plan/quick 矛盾。
904
968
  // 历史教训:曾因此让 agent 在 brainstorm 完成后被误导去跑 scan。
905
969
  const _bscale = readDesignScale(specBase, changeName)
906
- if (_bscale === 'small') {
907
- console.log(`\n👉 brainstorm 已完成(small)。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口)`)
970
+ if (_bscale === 'large') {
971
+ console.log(`\n👉 brainstorm 已完成(large)。下一步:sillyspec run plan${changeName ? ` --change ${changeName}` : ''}(完整五阶段)`)
908
972
  } else {
909
- console.log(`\n👉 brainstorm 已完成。下一步:sillyspec run plan${changeName ? ` --change ${changeName}` : ''}(scale=large 或未标 small 走完整 plan)`)
973
+ // 未标/small 默认收编轻量道(2026-09-29-brainstorm-exit-thin-default:此前「未标走完整 plan」
974
+ // 是缺省即厚;翻转与 thin-default-flip 同哲学——升厚留运行时证据+用户决策)
975
+ console.log(`\n👉 brainstorm 已完成(scale=${_bscale || '未标,默认 small'})。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口;实测失败自动升厚 / --upgrade-thick 用户决策)`)
910
976
  }
911
977
  } else if (stageName === 'quick') {
912
978
  // quick 是收尾阶段(辅助流程,不走主链 scan→archive),完成后该提交,而非推 scan。
@@ -1043,16 +1109,27 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
1043
1109
  mkdirSync(changeDir, { recursive: true })
1044
1110
  const { frozenPatch, ...snap } = result
1045
1111
  snap.note = 'execute --done 时点冻结(本文件落盘时采集)'
1046
- if (typeof frozenPatch === 'string') {
1047
- const patchText = frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n'
1048
- writeFileSync(join(changeDir, 'scope-audit.patch'), patchText)
1049
- snap.patchSha256 = createHash('sha256').update(patchText.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
1050
- snap.patchStatus = 'ok'
1051
- } else if (frozenPatch === null) {
1052
- snap.patchStatus = 'failed'
1053
- }
1054
- writeFileSync(join(changeDir, 'scope-audit.json'), JSON.stringify({ ...snap, savedAt: new Date().toISOString() }, null, 2) + '\n')
1055
- console.log(` 📦 范围快照已落变更目录(scope-audit.json${snap.patchStatus === 'ok' ? ' + scope-audit.patch(收尾时点冻结,sha256 已锚)' : snap.patchStatus === 'failed' ? '——patch 采集失败已留痕' : ''})`)
1112
+ snap.closedBy = 'execute --done'
1113
+ // 沉淀资产面投影(2026-10-07-unify-close-trace):files/totals = 主仓实改行(planned+
1114
+ // unplanned;untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓——均不入)。
1115
+ // 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处写四件(sha256 双套同锚);
1116
+ // 投影口径抽 flow-parity.projectTraceFaceRows 纯函数(排除路径有单测钉住)。
1117
+ const face = projectTraceFaceRows(snap.rows)
1118
+ const closeTrace = writeCloseTraceArtifacts({
1119
+ changeDir,
1120
+ change: changeName,
1121
+ baseline: typeof snap.baseAnchor === 'string' ? snap.baseAnchor : null,
1122
+ head: String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null,
1123
+ files: face.files,
1124
+ metaTotals: face.totals,
1125
+ patchText: typeof frozenPatch === 'string' && frozenPatch ? frozenPatch : null,
1126
+ savedAt: new Date().toISOString(),
1127
+ snapObj: snap,
1128
+ meta: {
1129
+ note: 'execute --done 时点冻结(沉淀资产面:主仓实改行投影——thin flow done 对等;治理工件目录与其后 verify/archive 演进不在内)',
1130
+ },
1131
+ })
1132
+ console.log(` 📦 范围快照 + 沉淀资产双轨落盘(scope-audit.json${closeTrace.patchStatus === 'ok' ? ' + scope-audit.patch' : ''} + change-patch.json${closeTrace.patchStatus === 'ok' ? ' + change.patch' : ''}${closeTrace.patchStatus === 'ok' ? '——收尾时点冻结,sha256 同锚' : '——patch 采集失败已留痕'})`)
1056
1133
  } catch (e) {
1057
1134
  console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
1058
1135
  }
@@ -2086,16 +2163,16 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
2086
2163
  // 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
2087
2164
  const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
2088
2165
  if (nextStageHint) {
2089
- // brainstorm 按 design.md frontmatter 的 scale 分叉(与末步 prompt 对齐):
2090
- // small → quick --linked-changes(小变更免走完整 plan);large / 读不到 scale → plan(保守默认)。
2091
- // 修历史 bug:此处曾硬编码 brainstorm→plan 不读 scale,small 档与末步 prompt(quick)矛盾、误导小变更进 plan。
2166
+ // brainstorm 按 design.md frontmatter 的 scale 分叉(2026-09-29-brainstorm-exit-thin-default
2167
+ // 与 completeStep 主路径同口径):large → run plan(完整五阶段);small/未标 → flow start
2168
+ // 收编轻量道(此前 small→quick --linked-changes 是旧收编通道、未标→plan 是缺省即厚,均已退役)。
2092
2169
  let hintStage = nextStageHint
2093
2170
  let hintChangeFlag = changeName ? ` --change ${changeName}` : ''
2094
- if (stageName === 'brainstorm' && changeName && readDesignScale(specBase, changeName) === 'small') {
2095
- hintStage = 'quick'
2096
- hintChangeFlag = ` --linked-changes ${changeName}`
2171
+ if (stageName === 'brainstorm' && changeName && readDesignScale(specBase, changeName) !== 'large') {
2172
+ console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口)`)
2173
+ } else {
2174
+ console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec run ${hintStage}${hintChangeFlag}`)
2097
2175
  }
2098
- console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec run ${hintStage}${hintChangeFlag}`)
2099
2176
  if (stageName === 'execute') {
2100
2177
  console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
2101
2178
  console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
@@ -722,6 +722,21 @@ export function detectSymlinkStoreLayout(cwd) {
722
722
  if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
723
723
  if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
724
724
  if (existsSync(join(cwd, 'lerna.json'))) return 'lerna'
725
+ // ── 子目录 lockfile(apps 型 monorepo——2026-09-30-snapshot-symlink-store-subdir)──
726
+ // 实证:multi-agent-platform(frontend/ + sillyhub-daemon/ 各自 pnpm-lock.yaml,根目录无
727
+ // lockfile)漏检 → 快照照建,verify 收敛循环每轮 ~160s 纯烧快照构建/junction。packages/*
728
+ // workspace 型不扫(该形态 lockfile 在根,根判据已覆盖)。子目录命中=该 app 的
729
+ // node_modules 符号链接网同样跨根失效——跳快照回主仓是既有保守裁决(宁主仓口径)。
730
+ try {
731
+ for (const e of readdirSync(cwd, { withFileTypes: true })) {
732
+ if (!e.isDirectory()) continue
733
+ if (e.name === 'node_modules' || e.name.startsWith('.')) continue
734
+ const d = join(cwd, e.name)
735
+ if (existsSync(join(d, 'pnpm-lock.yaml'))) return `pnpm(subdir:${e.name})`
736
+ if (existsSync(join(d, 'bun.lockb')) || existsSync(join(d, 'bun.lock'))) return `bun(subdir:${e.name})`
737
+ if (existsSync(join(d, 'lerna.json'))) return `lerna(subdir:${e.name})`
738
+ }
739
+ } catch { /* 根不可读(与非仓目录同语义)→ 落到 packageManager 判据 */ }
725
740
  try {
726
741
  const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))
727
742
  const pm = pkg && pkg.packageManager ? String(pkg.packageManager) : ''