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
package/src/run/stage.js CHANGED
@@ -21,6 +21,7 @@ import { existsSync, readdirSync, readFileSync, mkdirSync } from 'node:fs'
21
21
  import { writeAtomicSync } from '../fs-atomic.js'
22
22
  import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown, predictProtectedQuickFiles, mergeQuickBoundaryFiles, detectEmptyShellQuickSessions } from './shared.js'
23
23
  import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
24
+ import { executeProgressConfirm } from './progress-confirm.js'
24
25
  import { outputStep, collectStageWaitHistory } from './prompt.js'
25
26
  import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
26
27
  import { createHash } from 'node:crypto'
@@ -150,6 +151,9 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
150
151
  // marker 断裂(worktree cleanup / 归档清理 / 并行误删)后按戳归属,不再错配其他变更的 run
151
152
  const { stampExecuteRunChange } = await import('../task-review.js')
152
153
  stampExecuteRunChange(runtimeRoot, currentExecuteRunId, changeName)
154
+ // execute-runs 回收不在此处(ql-20260908-005/006 已定分野):变更归属类证据走归档时
155
+ // 精确回收(complete-handlers pruneArchivedChangeRuntime)+ doctor --gc-unstamped-runs
156
+ // 清存量;写入侧 keep-N 滚动是启发式,会误删活跃变更的旧 run,不进证据类目录。
153
157
  }
154
158
  }
155
159
 
@@ -399,6 +403,28 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
399
403
  // 判定与 --done 审计危险门同口径(predictProtectedQuickFiles)
400
404
  const protectedPreview = predictProtectedQuickFiles(allowedFiles, { linkedChanges, forceBaseline })
401
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
+ }
402
428
  // CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
403
429
  const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
404
430
  // 标题回退:启动 quick 不带 --input 时,从关联变更的 proposal/design 提取语义标题,
@@ -433,6 +459,9 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
433
459
  forceBaseline,
434
460
  linkedChanges,
435
461
  quicklogId: qlId,
462
+ // 刀①(2026-09-08):启动 --input 的任务描述落 guard——prompt.js 渲染 step1 时据此做
463
+ // 模块上下文关键词匹配(changeName 是 quick-<hash> 无语义,step.prompt 全文噪音大)
464
+ taskDescription: quickDesc,
436
465
  startedAt: new Date().toISOString(),
437
466
  }
438
467
  // 写入 .runtime/quick-sessions/<sessionId>/guard.json 供 worktree-guard hook 读取
@@ -537,6 +566,17 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
537
566
  await executeScanFinalize(cwd, platformOpts)
538
567
  } else if (cliAction === 'planPostcheck') {
539
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 */ }
576
+ } else if (cliAction === 'progressConfirm') {
577
+ // 主流程 step1「进度确认」noAI 化(2026-09-07):brainstorm/execute/verify——
578
+ // 快照本就 CLI 注入、阶段路由已由 run 落定,省一轮「复述摘要→--done」往返
579
+ executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
540
580
  } else {
541
581
  throw new Error(`noAI 步骤 ${stepName} 的未知 _cliAction: ${cliAction}——请在 stage.js 注册对应分支`)
542
582
  }
@@ -552,6 +592,8 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
552
592
  // 所有步骤完成
553
593
  stageData.status = 'completed'
554
594
  stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
595
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08;与 complete.js 两处同语义)
596
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
555
597
  // persist _write 移到 completeStageGates 成功之后(task-02 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
556
598
  // 阶段完成收尾共享管线(noAI 末步核心修复 S1:plan postcheck independent-tier review verdict=fail /
557
599
  // 平台 scan manifest 此前被绕过)。gate 失败已 rollback 为 in-progress,early-return(不 fall through 到末尾 return)。
@@ -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
+ }
@@ -42,7 +42,11 @@ function checkScanDocRefs(cwd, scanDir, docs) {
42
42
  severity: CHECK_SEVERITY.WARNING,
43
43
  detail: `${invalid.length} 条 file:line 引用未通过核验(${totalRefs} 条中): ${sample}${invalid.length > 3 ? ` 等 ${invalid.length} 条` : ''}`,
44
44
  evidence: { invalidCount: invalid.length, totalRefs, skippedCrossRepo, checkedDocs: docs.length, sample: invalid.slice(0, 3) },
45
- supportedFixes: ['运行 sillyspec docs check --suggest 查看候选锚点后修正引用', '删除无法核验的引用(引用了不存在的文件/行号)'],
45
+ supportedFixes: [
46
+ 'sillyspec docs check --paths <该文档> --dry-run 查看候选锚点(fixable 条目会给出 newLine),确认后去 --dry-run 落盘重锚',
47
+ 'sillyspec docs check --paths <该文档> --fix 对 fixable 引用自动重锚(回执附修复前后失效数对比)',
48
+ '先跑 sillyspec docs check --paths <该文档> 确认零候选锚点后,再删除确认无法核验的引用行',
49
+ ],
46
50
  }
47
51
  }
48
52
 
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 角色规则全手写。
@@ -28,7 +28,8 @@ export const definition = {
28
28
  ### 加载上下文
29
29
  4. 读取项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)+ 共享规范 + 子项目上下文
30
30
  5. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
31
- 6. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`
31
+ 6. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
32
+ {LOCAL_COMMANDS}
32
33
  7. 棕地项目:读取 scan 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
33
34
  8. 加载模块索引:读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(如存在)
34
35
  9. 查看进行中的变更:\`ls .sillyspec/changes/ | grep -v archive\`
@@ -7,22 +7,13 @@ export const definition = {
7
7
  name: '进度确认',
8
8
  // 重命名自「状态检查」(③ status 消歧):老进度里的「状态检查」completed 由迁移逻辑承接,防 currentIdx 回跳
9
9
  migratedFrom: ['状态检查'],
10
- prompt: `检查当前变更的进度状态(快照已由 CLI 注入,勿再跑 progress show——那是全量输出,浪费上下文;\`sillyspec status\` 是项目级快照,也不推进流程)。
11
-
12
- ### 进度快照(CLI 注入)
13
- {PROGRESS_SNAPSHOT}
14
-
15
- ### 操作
16
- 1. **检查变更名称是否有意义**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change-a3f2b7c1\`),询问用户确认实际变更名,然后运行 \`sillyspec change-rename <旧名> <新名>\` 重命名
17
- 2. 如果快照显示阶段不符或未初始化,输出正确提示并停止
18
-
19
- ### 输出
20
- 当前状态摘要(1-2 句话)
21
-
22
- ### 注意
23
- - 以 CLI 注入快照为准,不要自行推断阶段
24
- - **不要用 mv 命令重命名变更目录**,必须使用 \`sillyspec change-rename\`,否则 DB 和目录会脱节`,
25
- outputHint: '状态摘要',
10
+ // noAI 化(2026-09-07):快照本就 CLI 注入、阶段路由由 run 落定、变更名自动生成检测为机械
11
+ // 正则(命中只打印 rename 建议,agent 在工具结果可见、可在后续步骤交互改名)——省一轮
12
+ // 复述往返。步骤名保持不变(存量进度库按名匹配迁移)。
13
+ noAI: true,
14
+ _cliAction: 'progressConfirm',
15
+ prompt: '',
16
+ outputHint: '状态摘要(CLI 快照输出)',
26
17
  optional: false
27
18
  },
28
19
  {
@@ -34,7 +25,8 @@ export const definition = {
34
25
  - 漂移事实(CLI 算):{SCAN_STALENESS}
35
26
  - 机械事实底稿(CLI 注入,无 scan 底稿则为空):{SCAN_FACTS}
36
27
  2. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
37
- 3. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`
28
+ 3. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
29
+ {LOCAL_COMMANDS}
38
30
  4. 棕地项目:读取 {SPEC_ROOT}/docs/<project>/scan/ 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
39
31
  5. **加载模块索引**:读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(如存在)
40
32
  - 这一步是高频操作,_module-map.yaml 回答"哪个文件属于哪个模块、模块之间怎么依赖"
@@ -164,6 +156,7 @@ export const definition = {
164
156
 
165
157
  **决策记录(增量落盘,防会话中断丢失)**:每解决一个有实现影响的问题(不要把闲聊都记录进去),**当场追加**写入 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`——不要只在对话里留草稿等「写设计文档」步统一落盘:会话随时可能中断,未落盘的决策无法跨会话恢复。落盘规则:
166
158
 
159
+ - **优先跑 \`sillyspec fourpiece-init --change <change-name>\` 生成骨架再填语义**(2026-09-09 起:proposal/requirements/decisions 的 frontmatter/章节标题由 CLI 预生成,已存在不覆盖——手写 frontmatter/章节漏项是格式返工根源);design.md 骨架归 \`sillyspec design-init\`
167
160
  - 文件不存在则先创建:变更目录缺失先 \`mkdir -p {SPEC_ROOT}/changes/<change-name>\`;文件头部写 YAML frontmatter(\`author: <git-user>\` / \`created_at: <now-datetime>\`)+ 标题 \`# 决策记录(Decisions)\`
168
161
  - 每条记录追加到文件末尾,格式如下:
169
162
 
@@ -362,14 +355,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
362
355
  - 否决理由:一句话说明为何否决(status=rejected 时必填)
363
356
  - 复潮条件:什么前提下可重新考虑该决策(status=rejected 时必填)
364
357
  - 长期术语只在 archive/scan 时再提升到 \`{SPEC_ROOT}/docs/<project>/glossary.md\`
365
- 4. 格式自检(只查章节齐全;语义一致性/可行性/YAGNI 不在本步查,交给下一步 Design Grill 独立审查):
366
- - design.md 含全部必填章节(背景/设计目标/非目标/总体方案/文件变更清单/接口定义/风险登记)
367
- - design.md 头部 YAML frontmatter 字段齐全(author/created_at/scale 等)——「生成规范文件」步完成契约硬要求,审查前就补齐避免后续 docHash 漂移
368
- - design.md 含「自审」字面命中章节(自审 / Self-Review / Self-review)——「生成规范文件」步完成契约 brainstorm.design.self-review 硬要求,写在本步自审段即可
369
- - 如存在 decisions.md,design.md 是否引用所有当前版本 D-xxx@vN
370
- - 涉及 session/lease/agent_run/daemon/lifecycle 等关键词时,是否含「生命周期契约表」
371
- - UI 原型核对:文件清单含前端文件(.vue/.tsx/.jsx/.svelte/.html/.css 等)且改动达到「必须生成/建议生成」原型级别(分级见「分段展示设计」步)时,变更目录应已有 \`prototype-*.html\`——没有则本步补生成;确不生成时把跳过原因记入 design.md 风险登记,不得静默缺位
372
- 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 已就位)即天然合规
373
359
 
374
360
  ### 输出
375
361
  design.md 文件路径 + 自审结果