sillyspec 3.28.3 → 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 (56) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +24 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/src/archive-delta.js +82 -0
  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 +3 -1
  20. package/src/docs-check.js +34 -0
  21. package/src/doctor-diagnostics.js +441 -5
  22. package/src/foreign-declared.js +53 -2
  23. package/src/index.js +181 -6
  24. package/src/module-impact.js +77 -12
  25. package/src/plan-adopt-waves.js +13 -5
  26. package/src/progress/change-registry.js +16 -0
  27. package/src/progress.js +62 -1
  28. package/src/quick-recommend.js +6 -1
  29. package/src/quicklog.js +10 -2
  30. package/src/run/command.js +30 -9
  31. package/src/run/complete-handlers.js +152 -6
  32. package/src/run/complete.js +45 -0
  33. package/src/run/gates.js +94 -6
  34. package/src/run/prompt.js +96 -5
  35. package/src/run/shared.js +74 -6
  36. package/src/run/stage.js +35 -0
  37. package/src/runtime-hygiene.js +84 -0
  38. package/src/spec-sync.js +63 -8
  39. package/src/stage-contract.js +83 -3
  40. package/src/stage-review.js +3 -0
  41. package/src/stages/archive.js +4 -4
  42. package/src/stages/brainstorm.js +2 -8
  43. package/src/stages/doctor.js +41 -515
  44. package/src/stages/plan-postcheck.js +191 -7
  45. package/src/stages/plan.js +5 -26
  46. package/src/stages/quick.js +11 -11
  47. package/src/stages/verify.js +10 -6
  48. package/src/sync-noise.js +158 -0
  49. package/src/sync.js +42 -17
  50. package/src/task-review.js +25 -1
  51. package/src/verify-facts-schema.js +141 -0
  52. package/src/verify-postcheck.js +257 -19
  53. package/src/verify-probes.js +142 -13
  54. package/src/workflow.js +6 -0
  55. package/src/worktree-apply.js +51 -13
  56. package/src/worktree.js +5 -1
package/src/run/shared.js CHANGED
@@ -338,6 +338,8 @@ export function resolveQuickSessionsDir(platformOpts, localSpecBase) {
338
338
  // GHOST_EMPTY_DIR_STALE_MS 同口径(7 天)——正常并发会话间隔在分钟/小时级,7 天未收尾的
339
339
  // 会话按僵尸处理,防崩溃残留的 guard 永久「认领」文件侵蚀危险门。
340
340
  const FOREIGN_SESSION_STALE_MS = 7 * 24 * 60 * 60 * 1000
341
+ // 空壳会话点名宽限期:零步骤完成但启动未满此时长的不列(进行中会话的正常起步态)
342
+ const EMPTY_SHELL_GRACE_MS = 10 * 60 * 1000
341
343
 
342
344
  /**
343
345
  * 枚举其他 active quick 会话的显式文件声明(坑 foreign-session-declared-false-block)。
@@ -424,6 +426,9 @@ export function detectEmptyShellQuickSessions(platformOpts, localSpecBase, curre
424
426
  if (!guard || !guard.startedAt) continue
425
427
  const startedAtMs = Date.parse(guard.startedAt)
426
428
  if (!Number.isFinite(startedAtMs) || nowMs - startedAtMs > FOREIGN_SESSION_STALE_MS) continue
429
+ // 宽限期(2026-09-09 §7-4,会话实证:2 分钟前刚启动的进行中会话被点名疑似空壳):
430
+ // 零步骤完成对刚起步的会话是正常态,不满 10 分钟不列(宁可漏报不误报的同款取向)。
431
+ if (nowMs - startedAtMs < EMPTY_SHELL_GRACE_MS) continue
427
432
  try {
428
433
  const p = pm.read(cwd, sessionName)
429
434
  if (!p || !p.stages) continue
@@ -472,6 +477,16 @@ export function writePlatformPointer(cwd, platformOpts, extra = {}) {
472
477
  console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已跳过指针写入并按本地模式运行: ${platformOpts.specRoot}`)
473
478
  return false
474
479
  }
480
+ // temp 残留写侧守卫(2026-09-08 temp 投毒治理,防患于未然):specRoot 在系统 temp 且
481
+ // 项目 cwd 不在 temp → 拒绝三写。真实平台 spec 根永不在 temp;此类组合只可能来自
482
+ // 测试/联调以临时 specRoot 在真实项目根跑平台模式命令——指针/声明一旦落盘,指针被
483
+ // 清理后残留声明会让全命令 fail-closed(读侧 isTempResidueSpecRoot 降级兜底已存在
484
+ // 的污染,此处从源头断新增)。cwd 同在 temp(套件隔离正常形态)不拦,平台模式
485
+ // 测试不受影响。
486
+ if (isTempResidueSpecRoot(cwd, platformOpts.specRoot)) {
487
+ console.warn(`⚠️ 拒绝写入平台指针:specRoot 指向系统 temp 目录(${platformOpts.specRoot})而项目不在 temp 下——测试/联调残留特征,已跳过三写并按本地模式运行`)
488
+ return false
489
+ }
475
490
  // HUB-05:合并保留既有生命周期字段。scan 完成时指针被写入 status/completedAt/scanStatus
476
491
  //(complete-handlers),但下一次任何平台模式 run(含只读 --status)都会重写指针——
477
492
  // 恢复链只回填 specRoot 等四字段,不保留 status → 指针永远回 active、isPointerStale
@@ -569,6 +584,38 @@ export function isSelfReferentialSpecRoot(cwd, specRoot) {
569
584
  }
570
585
  }
571
586
 
587
+ /**
588
+ * temp 残留 specRoot 判定(temp 投毒治理,2026-09-08:multi-agent-platform 工作台
589
+ * 「总览不可用」排障实证——Git Bash mktemp 临时 specRoot 的平台模式命令在真实项目根
590
+ * 写下指针+声明,指针随后被 cleanup/STALE 清理,声明按设计存活(唯一删除路径 =
591
+ * platform disconnect)→ 全命令 fail-closed,且 disconnect 会连带清 local.yaml
592
+ * platform 段误伤真实平台连接)。
593
+ *
594
+ * 判定:specRoot 位于系统 temp 目录 **且** cwd 不在 temp 下 → true。两个条件缺一不可:
595
+ * 真实平台的 spec 根永不在 temp(daemon 用 ~/.sillyhub/daemon/specs/{ws},服务器用
596
+ * /data/spec-workspaces);测试/联调的 temp specRoot 只允许配 temp 项目 cwd(套件隔离
597
+ * 正常形态,见 test/run-tests.mjs childEnv 与 platform-managed-declaration 夹具——
598
+ * cwd 与 spec 同在 temp 下时本函数返回 false,既有 fail-closed 测试语义不变)。
599
+ *
600
+ * 与 isSelfReferentialSpecRoot 并列为声明的两类「可证伪残留」:自指回环(repo-native
601
+ * junction 投毒)已有降级先例(run/command.js:429),本函数补 temp 残留这一类。
602
+ * 口径对齐 progress.js 指针路径的 temp 警告(resolve() 前缀比较,不 realpath——temp
603
+ * 判定是残留启发式,无需穿透 junction 的强一致)。
604
+ *
605
+ * @param {string} cwd - 项目根(与 isSelfReferentialSpecRoot 同基准)
606
+ * @param {string|null} [specRoot] - 平台 specRoot(可 null/undefined/空串)
607
+ * @returns {boolean} 是否 temp 残留(写侧应拒绝三写,读侧应降级本地并清理声明)
608
+ */
609
+ export function isTempResidueSpecRoot(cwd, specRoot) {
610
+ if (!specRoot || typeof specRoot !== 'string') return false
611
+ const tmp = resolve(os.tmpdir())
612
+ const sr = resolve(specRoot)
613
+ if (sr !== tmp && !sr.startsWith(tmp + sep)) return false
614
+ const c = resolve(cwd)
615
+ if (c === tmp || c.startsWith(tmp + sep)) return false
616
+ return true
617
+ }
618
+
572
619
  /**
573
620
  * 平台模式判定单一数据源(自指免疫,变更 2026-08-23-repo-native-spec-backfill)。
574
621
  *
@@ -774,7 +821,8 @@ export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {})
774
821
  if (!sm._getPlatform()) return
775
822
  if (_autoPullRecently(cwd)) return
776
823
  _stampAutoPull(cwd)
777
- await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal }), opts.timeoutMs ?? AUTO_PULL_TIMEOUT_MS)
824
+ // autoPull:噪音闸标识(sync.js pull 的失败 warn 走跨进程静默窗口,见 sync-noise.js)
825
+ await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal, autoPull: true }), opts.timeoutMs ?? AUTO_PULL_TIMEOUT_MS)
778
826
  } catch (e) {
779
827
  // pull 失败静默跳过(Best Effort,失败不影响正确性)
780
828
  console.warn('⚠️ 拉取失败:', e.message)
@@ -813,9 +861,10 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
813
861
  }
814
862
  if (!cn) return
815
863
  // 已确认连接 + 单活跃变更,调 pull(自动注入 2s 熔断 + abort(HUB-09);skipIfLocalDirty 保守守卫
816
- // 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)
864
+ // 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)。
865
+ // autoPull:噪音闸标识(sync.js pull 的失败 warn 走跨进程静默窗口,见 sync-noise.js)
817
866
  try {
818
- await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal }), AUTO_PULL_TIMEOUT_MS)
867
+ await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal, autoPull: true }), AUTO_PULL_TIMEOUT_MS)
819
868
  } catch (e) {
820
869
  console.warn('⚠️ 拉取失败:', e.message)
821
870
  }
@@ -884,8 +933,9 @@ export function parsePorcelainPath(line) {
884
933
  }
885
934
 
886
935
  /**
887
- * quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion 审计 与 QUICKLOG「文件:」行回填
888
- * 单源)。回填文件行时复用:review.changedFiles 含这些元数据时过滤掉,只留真实业务文件。
936
+ * quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion 审计豁免的单一真相源)。
937
+ * 注意:QUICKLOG「文件:」行回填**不再直接复用**本谓词——记录面与审计豁免面已分叉
938
+ * (模块卡 + changelog sidecar 审计照旧放行、但要进文件行),见 isQuicklogFileLineNoise。
889
939
  *
890
940
  * 归类口径:
891
941
  * - quicklog/.runtime/modules/_module-map/knowledge-uncategorized 等 quick 自身产物 → 元数据
@@ -894,7 +944,7 @@ export function parsePorcelainPath(line) {
894
944
  *
895
945
  * 「并发工作 vs 偷建变更」的意图软判定留给 sillyhub,确定性校验只做路径归类。
896
946
  *
897
- * @param {string} p 文件路径(容错 \\ / 混用)
947
+ * @param {string} p 文件路径(容错 \ / 混用)
898
948
  * @param {string[]} [linkedChanges] 关联变更名列表(影响 changes/ 归类)
899
949
  * @returns {boolean} true=quick 元数据,应从业务文件列表过滤掉
900
950
  */
@@ -915,6 +965,24 @@ export function isQuickMetadata(p, linkedChanges = []) {
915
965
  return false
916
966
  }
917
967
 
968
+ /**
969
+ * QUICKLOG「文件:」行回填过滤——isQuickMetadata 的**记录面**变体(2026-09-08 用户反馈①:
970
+ * 审计按 .sillyspec/ 放行规则整体豁免没问题,但模块卡 + changelog sidecar 恰是收尾必改项,
971
+ * 被同一谓词从文件行滤掉后每回都得手工补录)。审计豁免面(isQuickMetadata)与记录面在此
972
+ * 分叉:quick 自身元数据(quicklog/.runtime/知识库未归类)照旧不入文件行,但本 quick 实际
973
+ * 改动的模块卡(`modules/<id>.md`)与 sidecar(`modules/<id>.changelog.md`,同被上方
974
+ * `[^/]+\.md$` 覆盖)保留——它们是真实收尾工作的一部分。`_module-map.yaml` 仍滤:CLI
975
+ * (module-resolve)维护的索引,非 agent 手工改动。
976
+ * @param {string} p 文件路径(容错 \ / 混用)
977
+ * @param {string[]} [linkedChanges] 关联变更名列表(透传 isQuickMetadata)
978
+ * @returns {boolean} true=元数据噪音,应从「文件:」行过滤掉
979
+ */
980
+ export function isQuicklogFileLineNoise(p, linkedChanges = []) {
981
+ const file = String(p).replace(/\\/g, '/')
982
+ if (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file)) return false
983
+ return isQuickMetadata(p, linkedChanges)
984
+ }
985
+
918
986
  // 危险文件清单(auditQuickCompletion 危险门与 predictProtectedQuickFiles 预告共用单一真相源)。
919
987
  // SillySpec 核心流程代码。W6 重构后 src/run.js(23 行 barrel)/ src/progress.js(facade)
920
988
  // 把真正逻辑下沉到 src/run/、src/progress/ 子目录——旧的精确文件名匹配
package/src/run/stage.js CHANGED
@@ -151,6 +151,9 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
151
151
  // marker 断裂(worktree cleanup / 归档清理 / 并行误删)后按戳归属,不再错配其他变更的 run
152
152
  const { stampExecuteRunChange } = await import('../task-review.js')
153
153
  stampExecuteRunChange(runtimeRoot, currentExecuteRunId, changeName)
154
+ // execute-runs 回收不在此处(ql-20260908-005/006 已定分野):变更归属类证据走归档时
155
+ // 精确回收(complete-handlers pruneArchivedChangeRuntime)+ doctor --gc-unstamped-runs
156
+ // 清存量;写入侧 keep-N 滚动是启发式,会误删活跃变更的旧 run,不进证据类目录。
154
157
  }
155
158
  }
156
159
 
@@ -400,6 +403,28 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
400
403
  // 判定与 --done 审计危险门同口径(predictProtectedQuickFiles)
401
404
  const protectedPreview = predictProtectedQuickFiles(allowedFiles, { linkedChanges, forceBaseline })
402
405
  if (protectedPreview.length > 0) warnProtectedQuickFiles(protectedPreview)
406
+ // 任务描述驱动预检(2026-09-09 刀批,轮次经济学 §3.4):未预声明 --files 且未带
407
+ // forceBaseline 时,用当前脏文件 × 任务描述 token 粗匹配猜可能触及的文件,命中
408
+ // 受保护面即起步点破——省「--done 撞危险门 → 带 --force-baseline 重跑」一整轮。
409
+ // 启发式 advisory:猜错零代价(误报忽略),猜中省一轮;精确判定仍归 --done 审计。
410
+ if (protectedPreview.length === 0 && !forceBaseline) {
411
+ try {
412
+ const desc = quickOpts?.taskDescription || ''
413
+ if (desc) {
414
+ const dirty = String(safeGit(cwd, ['status', '--porcelain', '--untracked-files=all']).value || '')
415
+ .split('\n').map(l => l.slice(3).trim().split(' -> ').pop() || '')
416
+ .map(pp => pp.replace(/^"|"$/g, '').replace(/\\/g, '/'))
417
+ .filter(pp => pp && !pp.startsWith('.sillyspec/'))
418
+ const tokens = desc.toLowerCase().split(/[\s,,。;;::/\\]+/).filter(t => t.length >= 3)
419
+ const guessed = dirty.filter(pp => tokens.some(t => pp.toLowerCase().includes(t) || t.includes(pp.split('/').pop().replace(/[.][a-z]+$/, '').toLowerCase())))
420
+ const guessedProtected = predictProtectedQuickFiles(guessed, { linkedChanges, forceBaseline })
421
+ if (guessedProtected.length > 0) {
422
+ console.warn(`⚠️ 任务描述可能触及受保护文件(启发式预检,精确判定在 --done):${guessedProtected.slice(0, 3).join('、')}${guessedProtected.length > 3 ? ' …' : ''}`)
423
+ console.warn(' 若确要改它们:重启 quick 带 --force-baseline 预声明,省 --done 被拦重跑一轮;误报忽略即可。')
424
+ }
425
+ }
426
+ } catch { /* 预检 fail-open */ }
427
+ }
403
428
  // CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
404
429
  const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
405
430
  // 标题回退:启动 quick 不带 --input 时,从关联变更的 proposal/design 提取语义标题,
@@ -434,6 +459,9 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
434
459
  forceBaseline,
435
460
  linkedChanges,
436
461
  quicklogId: qlId,
462
+ // 刀①(2026-09-08):启动 --input 的任务描述落 guard——prompt.js 渲染 step1 时据此做
463
+ // 模块上下文关键词匹配(changeName 是 quick-<hash> 无语义,step.prompt 全文噪音大)
464
+ taskDescription: quickDesc,
437
465
  startedAt: new Date().toISOString(),
438
466
  }
439
467
  // 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
@@ -538,6 +566,13 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
538
566
  await executeScanFinalize(cwd, platformOpts)
539
567
  } else if (cliAction === 'planPostcheck') {
540
568
  await executePlanPostcheck(cwd, platformOpts, progress)
569
+ } else if (cliAction === 'doctorRunDiagnostics') {
570
+ // 2026-09-09-doctor-noai FR-01:doctor 阶段折叠——noAI 步跑全量诊断
571
+ // (八维 + 三新 detector)+ renderDoctorSummary 渲染 + doctor-diagnosis.json 落盘
572
+ const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('../doctor-diagnostics.js')
573
+ const diag = await runDoctorDiagnostics({ cwd })
574
+ console.log(renderDoctorSummary(diag))
575
+ try { writeDoctorDiagnosis(diag, (platformOpts?.specRoot || join(cwd, '.sillyspec'))) } catch { /* 落盘 fail-soft */ }
541
576
  } else if (cliAction === 'progressConfirm') {
542
577
  // 主流程 step1「进度确认」noAI 化(2026-09-07):brainstorm/execute/verify——
543
578
  // 快照本就 CLI 注入、阶段路由已由 run 落定,省一轮「复述摘要→--done」往返
@@ -0,0 +1,84 @@
1
+ /**
2
+ * runtime-hygiene.js — .runtime/ 无归属审计类产物的写入侧滚动裁剪(2026-09-08 用户反馈④)。
3
+ *
4
+ * 系统性排查后的分野(与 ql-20260908-005/006 并行会话的定界):
5
+ * - **变更归属类证据**(execute-runs / stage-reviews / verify-runs):走归档时按 change
6
+ * 精确回收(complete-handlers.js pruneArchivedChangeRuntime)+ doctor --gc-unstamped-runs
7
+ * 清存量。不在此处滚动裁剪——keep-N 是启发式,他端高频写入会把活跃变更的证据跌出
8
+ * 保留窗,gate(Stage/Task Review Gate)读到的是被裁掉的 review。
9
+ * - **无归属审计类**(本模块管辖):artifacts/(步骤超长输出存档,实证本仓 534 份)、
10
+ * workflow-runs/(workflow check 归档,21 份)、doctor-dumps/(孤儿库诊断 dump)。
11
+ * 无生命周期事件可挂(无「归档」时机)、无 change 归属语义,只有时间价值——
12
+ * 写入侧滚动保留最新 N 份是唯一合适量身工具(与 _pruneImportBaks 同族:靠人工
13
+ * 记得跑 GC 等于没有回收)。
14
+ *
15
+ * 契约(与 _pruneImportBaks 对齐的取舍):
16
+ * - 新旧判据默认取**条目名字典序**:管辖目录的条目名全部内嵌零填充时间戳
17
+ * (<ts>-<label>.json / dump-<ts>.json),同目录内字典序 == 时间序;不用 mtime——
18
+ * copy/同步工具会改写。例外:artifacts/ 文件名时间戳在尾部、前缀是变更名,长变更
19
+ * 晚收尾会乱序,按 mtime(write-once 本地审计文件,无 copy 改写 mtime 的暴露面)。
20
+ * - tsRe 选项供前缀不统一的目录提取内嵌时间戳(排序键),提取不到的条目沉底不裁。
21
+ * - 单条删除失败(他进程持句柄/权限)跳过不连坐,下次写入再收。
22
+ * - 整体 fail-open:目录缺失/读取异常返回 0,绝不抛——卫生动作不能阻断主流程。
23
+ * - 幂等:keep 之内不动,重复调用零副作用。
24
+ * - 保留份数可用 SILLYSPEC_RUNTIME_KEEP 覆盖(整数 ≥1,灾难排查期想多留),
25
+ * 非法值回落各调用方默认。
26
+ */
27
+ import { readdirSync, statSync, rmSync } from 'fs'
28
+ import { join } from 'path'
29
+
30
+ /**
31
+ * 读环境覆盖的保留份数。
32
+ * @param {number} def 调用方默认值
33
+ * @returns {number}
34
+ */
35
+ function _resolveKeep(def) {
36
+ const raw = Number(process.env.SILLYSPEC_RUNTIME_KEEP)
37
+ return Number.isInteger(raw) && raw >= 1 ? raw : def
38
+ }
39
+
40
+ /**
41
+ * 滚动裁剪一个「时间戳命名条目」目录,保留最新 keep 份。
42
+ *
43
+ * @param {object} opts
44
+ * @param {string} opts.dir 目标目录(如 <specBase>/.runtime/artifacts)
45
+ * @param {number} opts.keep 默认保留份数(SILLYSPEC_RUNTIME_KEEP 可覆盖)
46
+ * @param {'name'|'mtime'} [opts.orderBy='name'] 新旧判据;'mtime' 仅供时间戳不前置的
47
+ * 条目(artifacts/:前缀是变更名,变更名自带日期近似但不严格时序)
48
+ * @param {RegExp} [opts.tsRe] 从条目名提取时间戳的正则(捕获排序键)。前缀不统一的
49
+ * 目录必须传,否则纯名字典序会跨前缀乱序误裁;提取不到的条目按名字参与排序
50
+ * (保守沉底不误删)
51
+ * @returns {number} 实际删除的条目数(目录缺失/异常 = 0,绝不抛)
52
+ */
53
+ export function pruneTimestampedEntries({ dir, keep, orderBy = 'name', tsRe = null }) {
54
+ let entries
55
+ try {
56
+ entries = readdirSync(dir, { withFileTypes: true })
57
+ } catch {
58
+ return 0 // 目录不存在(该仓库从未走过此流程)= 无可裁
59
+ }
60
+ const names = entries.map(e => e.name)
61
+ const sortKey = orderBy === 'mtime'
62
+ ? (n) => { try { return statSync(join(dir, n)).mtimeMs } catch { return 0 } }
63
+ : (n) => {
64
+ if (tsRe) {
65
+ const m = n.match(tsRe)
66
+ if (m) return m[0] // 内嵌零填充时间戳(如 20260908035502)
67
+ }
68
+ return `\uffff${n}` // 提取不到 → 沉底(视为最新,不因缺时间戳被裁)
69
+ }
70
+ const sorted = [...names].sort((a, b) => {
71
+ const ka = sortKey(a), kb = sortKey(b)
72
+ return ka < kb ? -1 : ka > kb ? 1 : 0 // 升序:最旧在前
73
+ })
74
+ const k = _resolveKeep(keep)
75
+ const doomed = sorted.slice(0, Math.max(0, sorted.length - k))
76
+ let removed = 0
77
+ for (const n of doomed) {
78
+ try {
79
+ rmSync(join(dir, n), { recursive: true, force: true })
80
+ removed++
81
+ } catch { /* 单条失败跳过:下次写入侧再收 */ }
82
+ }
83
+ return removed
84
+ }
package/src/spec-sync.js CHANGED
@@ -18,6 +18,7 @@
18
18
  import { createHash } from 'crypto';
19
19
  import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
20
20
  import { join, relative, sep } from 'path';
21
+ import { bindSyncNoiseRoot, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess } from './sync-noise.js';
21
22
 
22
23
  // 与 sillyhub-daemon/src/spec-sync.ts 共用排除口径(task-07 / FR-06 / D-008@v2):
23
24
  // .runtime(有点)/ runtime(无点)/ projects(本地环境文件)在顶层排除;
@@ -457,6 +458,41 @@ function clearSpecConflictMarker(specRoot, changeName) {
457
458
  } catch { /* 清理失败不影响同步结果 */ }
458
459
  }
459
460
 
461
+ // follower/stale 提示的跨轮去重(坑 spec-sync-follow-banner-spam,2026-09-08 用户反馈③):
462
+ // 「N 个本地未改动文件自动跟随服务器」的信息只在集合**首次出现/变化**时有价值。基线快照锚定
463
+ // local-at-last-sync(见 writeBaseSnapshot 注释),本地落后于服务器的文件每轮都重新算出同样的
464
+ // follower 集合(computeSpecOps 生成 op → dropFollowServerUpdates 再丢掉),平台未就绪 POST
465
+ // 失败时基线还不落盘 → 同一集合每步重刷,实证 57 文件跟随逐条命令刷屏。marker 记上次已报集合,
466
+ // 未变则降 debugLog;集合清空时清 key(此后同集合再现算「变化」重新报)。
467
+ const FOLLOW_REPORT_MARKER = 'spec-sync-follow-reported.json';
468
+
469
+ /**
470
+ * 判定 follower/stale 集合相对上次已报告是否变化(变化时更新 marker)。
471
+ * @param {string} specRoot
472
+ * @param {'follow'|'stale'} key
473
+ * @param {string[]} list 本轮集合
474
+ * @returns {boolean} true=有变化应报告;false=与上次相同应静默
475
+ */
476
+ export function isFollowerSetChanged(specRoot, key, list) {
477
+ const sorted = [...(list || [])].sort();
478
+ const p = join(specRoot, '.runtime', FOLLOW_REPORT_MARKER);
479
+ let cur = {};
480
+ try { cur = JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { /* 无 marker = 首报 */ }
481
+ const prev = Array.isArray(cur[key]) ? cur[key] : [];
482
+ const same = prev.length === sorted.length && prev.every((v, i) => v === sorted[i]);
483
+ if (same) return false;
484
+ try {
485
+ if (sorted.length === 0) delete cur[key];
486
+ else cur[key] = sorted;
487
+ if (Object.keys(cur).length === 0) { try { unlinkSync(p) } catch {} }
488
+ else {
489
+ mkdirSync(join(specRoot, '.runtime'), { recursive: true });
490
+ writeFileSync(p, JSON.stringify(cur) + '\n', 'utf8');
491
+ }
492
+ } catch { /* marker 失败 = 退化为逐轮报告(只损失降噪) */ }
493
+ return sorted.length > 0;
494
+ }
495
+
460
496
  /**
461
497
  * 同步本地 .sillyspec 树到平台(CLI 直跑增量同步入口)。
462
498
  *
@@ -470,6 +506,9 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
470
506
  debugLog('[spec-sync] 未连接平台(本地合法状态);跳过 spec 树增量同步');
471
507
  return { synced: 0 };
472
508
  }
509
+ // 绑定连接类失败噪音闸(sync-noise.js):本目录所有 [spec-sync]/[sync] 失败 warn 的
510
+ // 跨进程静默窗口 marker 落这里(与 SyncManager 构造器绑定同目录)。
511
+ try { bindSyncNoiseRoot(join(specRoot, '.runtime')); } catch { /* 直通降级 */ }
473
512
  // HUB-09:单请求超时与外部熔断 signal 合并——trigger* 熔断时在飞请求被真实取消
474
513
  const withSignal = (timeoutMs) => (opts.signal ? AbortSignal.any([AbortSignal.timeout(timeoutMs), opts.signal]) : AbortSignal.timeout(timeoutMs));
475
514
 
@@ -488,13 +527,18 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
488
527
  // SILLYSPEC_DEBUG_SYNC=1 才可见),文件迟到平台且无任何线索(multi-agent-platform
489
528
  // 2026-08-18-workspace-file-browser 实证:design/decisions 迟到 27 分钟、plan.md 迟到
490
529
  // 8 分钟才被后续步骤的同步补上)。失败可见、成功不打扰。
491
- console.warn(`[spec-sync] 拉取清单失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${changeName}`);
530
+ // 连接类失败(404=端点未就绪 / 5xx)走噪音闸(sync-noise.js):平台未就绪期每步
531
+ // 自动同步重刷同一行,淹没门禁输出;其余状态码(4xx 业务态)每轮该看见。
532
+ const line = `[spec-sync] 拉取清单失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${changeName}`;
533
+ if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
534
+ else console.warn(line);
492
535
  return { synced: 0 };
493
536
  }
494
537
  const body = await res.json().catch(() => ({}));
495
538
  serverManifest = body.files || {};
496
539
  } catch (err) {
497
- console.warn(`[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`);
540
+ const line = `[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`;
541
+ syncConnectionWarn(line); // 网络/超时/熔断均连接类
498
542
  return { synced: 0 };
499
543
  }
500
544
 
@@ -513,13 +557,19 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
513
557
  if (!opts.forcePush) {
514
558
  const dropped = dropFollowServerUpdates(ops, localFiles, baseHashes);
515
559
  if (dropped.followed.length > 0) {
516
- console.log(
517
- `[spec-sync] ${dropped.followed.length} 个本地未改动文件自动跟随服务器(他端推进,内容未变不回推): ${dropped.followed.slice(0, 5).join(', ')}${dropped.followed.length > 5 ? ' 等' : ''}`
518
- );
560
+ // 集合未变不重报(坑 spec-sync-follow-banner-spam):本地落后集合每轮都重算出,
561
+ // 平台未就绪期同一批文件逐条命令刷屏。集合变化(新增漂移/清空后再现)才报。
562
+ if (isFollowerSetChanged(specRoot, 'follow', dropped.followed)) {
563
+ console.log(
564
+ `[spec-sync] ${dropped.followed.length} 个本地未改动文件自动跟随服务器(他端推进,内容未变不回推): ${dropped.followed.slice(0, 5).join(', ')}${dropped.followed.length > 5 ? ' 等' : ''}`
565
+ );
566
+ } else {
567
+ debugLog(`[spec-sync] ${dropped.followed.length} 个本地未改动文件自动跟随服务器(集合与上次相同,不重报)`);
568
+ }
519
569
  }
520
570
  ops = dropped.ops;
521
571
  const filtered = filterStaleUpdates(ops, localFiles, lastSyncTs);
522
- if (filtered.stale.length > 0) {
572
+ if (filtered.stale.length > 0 && isFollowerSetChanged(specRoot, 'stale', filtered.stale)) {
523
573
  console.warn(
524
574
  `[spec-sync] 拦下 ${filtered.stale.length} 个旧副本回推(本地自上次同步未改动而服务器已前进,如确需以本地为准:重存后重推): ${filtered.stale.slice(0, 5).join(', ')}${filtered.stale.length > 5 ? ' 等' : ''}`
525
575
  );
@@ -544,7 +594,9 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
544
594
  signal: withSignal(30000),
545
595
  });
546
596
  if (!res.ok) {
547
- console.warn(`[spec-sync] 同步请求失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${changeName}`);
597
+ const line = `[spec-sync] 同步请求失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${changeName}`;
598
+ if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
599
+ else console.warn(line);
548
600
  return { synced: 0 };
549
601
  }
550
602
  const body = await res.json().catch(() => ({}));
@@ -586,6 +638,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
586
638
  writeLastSyncTs(specRoot);
587
639
  writeBaseSnapshot(specRoot, localFiles);
588
640
  clearSpecConflictMarker(specRoot, changeName);
641
+ noteSyncSuccess();
589
642
  console.log(`[spec-sync] 冲突已自动消解(0 个需人工裁决;本会话改动 ${ops.length - followers.length > 0 ? '已同步' : '无差异'}): ${changeName}`);
590
643
  return { synced: Math.max(ops.length - followers.length, 0), conflict: false, autoResolved: followers.length };
591
644
  }
@@ -614,6 +667,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
614
667
  console.warn(`⚠️ 处置:sillyspec platform resolve ${changeName} --keep-local | --take-platform | --abort(冲突详情: ${conflictPath || '(写入失败)'})`);
615
668
  return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath };
616
669
  }
670
+ noteSyncSuccess();
617
671
  console.log(`[spec-sync] 已同步 ${ops.length} 个文件变更: ${changeName}`);
618
672
  // 本轮无冲突 = 此前的未决 spec 树冲突已不存在,清残留标记(否则 status 永久红标、
619
673
  // 横幅去重单行提示不消——坑 spec-sync-conflict-banner-spam)
@@ -622,7 +676,8 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
622
676
  writeBaseSnapshot(specRoot, localFiles);
623
677
  return { synced: ops.length };
624
678
  } catch (err) {
625
- console.warn(`[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`);
679
+ const line = `[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`;
680
+ syncConnectionWarn(line); // 网络/超时/熔断均连接类
626
681
  return { synced: 0 };
627
682
  }
628
683
  }
@@ -10,6 +10,8 @@ import { join, basename } from 'path'
10
10
  import { safeGit } from './git-helper.js'
11
11
  import { nowWallClock } from './datetime.js'
12
12
  import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
13
+ import { parseEvidenceSlots } from './verify-facts-schema.js'
14
+ import { IR_STRICT_SINCE } from './constants.js'
13
15
  import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
14
16
  import { evaluateRules } from './stage-contract-engine.js'
15
17
  import { getRule } from './stage-contract-spec.js'
@@ -457,6 +459,10 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
457
459
  * 标题放宽:含「结论/Conclusion/Result/结果」的二级标题均可(B3c),
458
460
  * PASS/FAIL 可在标题行本身(如「## 验收结论:✅ PASS」)或紧邻标题的正文里。
459
461
  * 历史教训:原正则锚定确切「## 结论」,用户写「## 验收结论:✅ PASS」不被识别。
462
+ * 2026-09-08 刀③起降级为 legacy 回退:首选结论枚举槽(extractVerifyConclusionSlot),
463
+ * 仅当文件完全没有槽行时才走本窗口扫描(存量变更兼容);槽存在即不走本函数——
464
+ * 骨架占位符 `<待填:三选一>` 不含枚举词,两种解析下都 fail-closed(修掉旧占位符
465
+ * `<待填:PASS 或 FAIL>` 被窗口正则误读成 PASS 的自通过缺陷)。
460
466
  */
461
467
  function extractVerifyConclusion(verify) {
462
468
  // 遍历所有含关键词的二级标题,取其 400 字符窗口内含 PASS/FAIL 的那个(坑
@@ -481,6 +487,54 @@ function extractVerifyConclusion(verify) {
481
487
  return best || ''
482
488
  }
483
489
 
490
+ /**
491
+ * 结论枚举槽提取(刀③,2026-09-08):verify-probes --init 骨架生成的固定槽行
492
+ * `结论枚举:`<枚举值>…``——解析只认该行的枚举词,标题措辞劫持面归零。
493
+ * @param {string} verify - verify-result.md 全文
494
+ * @returns {string|null} 枚举值(PASS / PASS WITH NOTES / FAIL);'' = 有槽未填(fail-closed);
495
+ * null = 文件无槽行(调用方走 legacy 窗口扫描)
496
+ */
497
+ export function extractVerifyConclusionSlot(verify) {
498
+ if (!/^结论枚举:/im.test(verify)) return null
499
+ const m = verify.match(/^结论枚举:`?(PASS WITH NOTES|PASS|FAIL)\b/im)
500
+ // 交替序 PASS WITH NOTES 在前,防裸 PASS 抢先截断
501
+ return m ? m[1] : ''
502
+ }
503
+
504
+ /**
505
+ * 结论解析统一入口:槽优先,无槽走 legacy 窗口扫描并对存量格式发迁移提示。
506
+ * @param {string} verify - verify-result.md 全文
507
+ * @param {string[]} [warnings] - 传入时追加 legacy 迁移 warning
508
+ * @returns {string} 枚举值或 ''(未识别/未填)
509
+ */
510
+ function resolveVerifyConclusion(verify, warnings) {
511
+ const slot = extractVerifyConclusionSlot(verify)
512
+ if (slot !== null) return slot
513
+ if (warnings) {
514
+ warnings.push(
515
+ 'verify-result.md 结论未用「结论枚举:」槽行(旧自由格式,关键词窗口解析对标题措辞敏感——已有两次历史坑)。' +
516
+ '新变更请用 `sillyspec verify-probes --change <变更> --init` 骨架的槽格式;窗口解析仅为存量变更保留。'
517
+ )
518
+ }
519
+ return extractVerifyConclusion(verify)
520
+ }
521
+
522
+ /**
523
+ * IR 严格档判别(审核 P2-2):design.md frontmatter created_at ≥ IR_STRICT_SINCE。
524
+ * 与 verify-postcheck isStrictChange 同锚(常量单源 constants.js),此处只读 design
525
+ * 文件(不接 DB——validator 层保持纯文件判定,db 侧判别归 isStrictChange)。
526
+ */
527
+ function isIrStrictVerifyChange(changeDir) {
528
+ try {
529
+ const dp = join(changeDir, 'design.md')
530
+ if (!existsSync(dp)) return false
531
+ const fm = readFileSync(dp, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/)
532
+ const cm = fm && fm[1].match(/^created_at:\s*(\d{4}-\d{2}-\d{2})/m)
533
+ if (!cm) return false
534
+ return cm[1] >= IR_STRICT_SINCE
535
+ } catch { return false }
536
+ }
537
+
484
538
  function validateVerifyOutputs(cwd, changeName, context = {}) {
485
539
  const { specRoot } = context
486
540
  const changeDir = resolveChangeDir(cwd, changeName, specRoot)
@@ -509,7 +563,17 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
509
563
  // ── FAIL 结论门控(适用于所有变更,不限风险等级)──
510
564
  // verify-result.md 结论为 FAIL 时,verify 阶段不能 completed。
511
565
  // 历史教训:CLI 曾不校验结论,AI 写 FAIL 后 verify 仍被标记完成并提示"验证通过可以归档"。
512
- const conclusionStr = extractVerifyConclusion(verify)
566
+ // 刀③起结论解析槽优先(extractVerifyConclusionSlot),一处解析两处消费。
567
+ const conclusionStr = resolveVerifyConclusion(verify, warnings)
568
+ // ── 严格档结论槽必在(2026-09-09 外部审核 P2-2):有槽 fail-closed,但整行删「结论枚举:」
569
+ // 曾可逃回 legacy 窗口正则蹭关键词。IR 严格档变更(design created_at ≥ IR_STRICT_SINCE)
570
+ // 无槽直接 ERROR——存量变更不受影响(legacy 回退保留)。──
571
+ if (!/^结论枚举:/im.test(verify)) {
572
+ const strict = isIrStrictVerifyChange(changeDir)
573
+ if (strict) {
574
+ errors.push('严格档变更(created_at ≥ ' + IR_STRICT_SINCE + ')verify-result.md 缺「结论枚举:」槽行——删槽回退关键词窗口的通道已关闭。修复:跑 `sillyspec verify-probes --change <变更名> --init` 补槽段(已有正文不覆盖),把 <待填:三选一> 替换为枚举值。')
575
+ }
576
+ }
513
577
  if (conclusionStr === 'FAIL') {
514
578
  errors.push(getRule('verify.conclusion.fail-gate').failMessage)
515
579
  } else if (!conclusionStr) {
@@ -521,7 +585,7 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
521
585
  designContent: readIfExists(join(changeDir, 'design.md')),
522
586
  planContent: readIfExists(join(changeDir, 'plan.md')),
523
587
  })
524
- const conclusion = extractVerifyConclusion(verify)
588
+ const conclusion = conclusionStr // 槽优先解析结果复用(此前同输入重复扫两遍)
525
589
  // ── 否定抑制审计(坑 risk-negation-blindness)──
526
590
  // 关键词命中被同句否定语境抑制而不参与判级时必须透出:抑制是把双刃剑(「本次不新增
527
591
  // daemon」真豁免 vs 靠堆否定措辞静默降级逃证据门控),这里让每次抑制可见可核对;
@@ -573,7 +637,23 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
573
637
  }
574
638
  }
575
639
  } catch { /* 回执损坏按无回执处理(fail 回原字面匹配) */ }
576
- const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification, { extraEvidenceText: receiptText })
640
+ // v2(2026-09-08-ir-verify-facts FR-04):回执槽优先——verify-result.md「## 集成验证回执」
641
+ // 槽在场时传结构化 runtimeEvidence 走一致性校验(绿判据),无槽降 legacy literals。
642
+ // verifyStartAt 由调用方经 context 传入(gates 接线侧 ProgressManager.getStageCompletedAt
643
+ // 同步算好——runValidators 引擎是同步的,此处不做 DB IO;缺省 → auditRuntimeReceipt
644
+ // fail-soft 跳过 mtime 核验)。
645
+ let runtimeEvidence = null
646
+ try {
647
+ const slotsEv = parseEvidenceSlots(verify)
648
+ if (slotsEv.hasReceiptSlot) runtimeEvidence = slotsEv.runtimeEvidence
649
+ } catch { /* 解析失败按无槽 legacy */ }
650
+ const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification, {
651
+ extraEvidenceText: receiptText,
652
+ ...(runtimeEvidence ? { runtimeEvidence } : {}),
653
+ ...(context.verifyStartAt ? { verifyStartAt: context.verifyStartAt } : {}),
654
+ cwd,
655
+ specBase: context.specRoot || join(cwd, '.sillyspec'),
656
+ })
577
657
  if (!evidenceCheck.ok) {
578
658
  // A: 报错说人话 —— 把「缺哪一项、要写/做什么、判级原因」逐条列出,
579
659
  // 让 agent 不必靠改结论文案撞墙。detail 指明真实启动须是本变更实际改动的
@@ -633,6 +633,9 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
633
633
  const reviewPath = join(reviewDir, 'review.json')
634
634
  mkdirSync(reviewDir, { recursive: true })
635
635
  writeFileSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
636
+ // stage-reviews 回收不在此处(ql-20260908-005 已定分野):变更归属类证据走归档时精确
637
+ // 回收(pruneArchivedChangeRuntime 按 reviewedFiles 归属);写入侧滚动是启发式,
638
+ // 活跃变更的 review 可能因他端高频写而跌出 keep 窗,不进证据类目录。
636
639
 
637
640
  const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
638
641
  if (existsSync(markerPath)) {
@@ -30,10 +30,10 @@ export const definition = {
30
30
 
31
31
  ### 操作
32
32
  1. 读取 changes/<change>/module-impact.md(应已存在;large 变更 plan 阶段产出首版)
33
- 2. 运行 git diff --name-only HEAD~1(或 git diff --name-only --cached)获取真实修改文件列表
34
- 3. 三重核对:module-impact.md 记录的受影响模块/文件 vs 真实 git diff 文件列表 vs .sillyspec/docs/<project>/modules/_module-map.yaml 模块映射(不存在则仅核 unmapped 部分)。以 git diff 为准(真实 > 记录)
35
- 4. 发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)→ 直接修正 module-impact.md,使其与实际变更一致
36
- 5. module-impact.md 第一行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
33
+ 2. **机械核对已由 CLI 代算(2026-09-09 起)**:下方「三重核对报告」区块由 CLI 注入(module-impact 矩阵文件 × 真实 diff 三源文件 × module-map 归属的机器比对结果)——不要重跑 git diff 手工比对,直接消费报告:
34
+ {ARCHIVE_IMPACT_AUDIT}
35
+ 3. 报告标「不一致」的项才需要你裁决:漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标 → 直接修正 module-impact.md,使其与实际变更一致(以 git diff 为准——真实 > 记录);报告「一致 ✓」则跳过修正
36
+ 4. module-impact.md 第一行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
37
37
 
38
38
  ### 降级(module-impact.md 不存在)
39
39
  若缺失(如旧变更未经新 plan 流程,或 small 变更从未生成):先跑 \`sillyspec module-impact --change <change-name>\`——CLI 按 _module-map.yaml paths 前缀匹配预填「模块影响矩阵」骨架(文件×模块归属 + 未匹配文件清单,机械部分全代算),你只需逐行填「影响类型」(逻辑变更/数据结构变更/接口变更/调用关系变更/配置变更/新增)与 needs_review 标记,补首行中文标题 \`# 模块影响分析(Module Impact)— <变更简述>\`。骨架无从生成(无 module-map / 无 diff)时才按 archive-impact.yaml 的 impact-analyzer 角色规则全手写。
@@ -156,6 +156,7 @@ export const definition = {
156
156
 
157
157
  **决策记录(增量落盘,防会话中断丢失)**:每解决一个有实现影响的问题(不要把闲聊都记录进去),**当场追加**写入 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`——不要只在对话里留草稿等「写设计文档」步统一落盘:会话随时可能中断,未落盘的决策无法跨会话恢复。落盘规则:
158
158
 
159
+ - **优先跑 \`sillyspec fourpiece-init --change <change-name>\` 生成骨架再填语义**(2026-09-09 起:proposal/requirements/decisions 的 frontmatter/章节标题由 CLI 预生成,已存在不覆盖——手写 frontmatter/章节漏项是格式返工根源);design.md 骨架归 \`sillyspec design-init\`
159
160
  - 文件不存在则先创建:变更目录缺失先 \`mkdir -p {SPEC_ROOT}/changes/<change-name>\`;文件头部写 YAML frontmatter(\`author: <git-user>\` / \`created_at: <now-datetime>\`)+ 标题 \`# 决策记录(Decisions)\`
160
161
  - 每条记录追加到文件末尾,格式如下:
161
162
 
@@ -354,14 +355,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
354
355
  - 否决理由:一句话说明为何否决(status=rejected 时必填)
355
356
  - 复潮条件:什么前提下可重新考虑该决策(status=rejected 时必填)
356
357
  - 长期术语只在 archive/scan 时再提升到 \`{SPEC_ROOT}/docs/<project>/glossary.md\`
357
- 4. 格式自检(只查章节齐全;语义一致性/可行性/YAGNI 不在本步查,交给下一步 Design Grill 独立审查):
358
- - design.md 含全部必填章节(背景/设计目标/非目标/总体方案/文件变更清单/接口定义/风险登记)
359
- - design.md 头部 YAML frontmatter 字段齐全(author/created_at/scale 等)——「生成规范文件」步完成契约硬要求,审查前就补齐避免后续 docHash 漂移
360
- - design.md 含「自审」字面命中章节(自审 / Self-Review / Self-review)——「生成规范文件」步完成契约 brainstorm.design.self-review 硬要求,写在本步自审段即可
361
- - 如存在 decisions.md,design.md 是否引用所有当前版本 D-xxx@vN
362
- - 涉及 session/lease/agent_run/daemon/lifecycle 等关键词时,是否含「生命周期契约表」
363
- - UI 原型核对:文件清单含前端文件(.vue/.tsx/.jsx/.svelte/.html/.css 等)且改动达到「必须生成/建议生成」原型级别(分级见「分段展示设计」步)时,变更目录应已有 \`prototype-*.html\`——没有则本步补生成;确不生成时把跳过原因记入 design.md 风险登记,不得静默缺位
364
- 5. 缺章节 → 补齐后重检;章节齐全 → 进入下一步(Design Grill 做语义层交叉审查)
358
+ 4. **格式自检不做 agent 侧复刻**(2026-09-09 刀批:章节齐全/frontmatter/自审章节/D-xxx@vN 引用/生命周期豁免/UI 原型六项全部由 CLI 硬校验承担——「生成规范文件」步 --done 的 brainstorm.design.* 规则 + plan 阶段 validateDesignForPlan 同款字面检查,漏项门禁会逐条点名,复检是浪费轮)。design.md 骨架用 \`sillyspec design-init\` 生成(章节/frontmatter 已就位)即天然合规
365
359
 
366
360
  ### 输出
367
361
  design.md 文件路径 + 自审结果