sillyspec 3.26.13 → 3.26.15

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-archive/SKILL.md +2 -0
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
  4. package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
  7. package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
  8. package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
  9. package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
  11. package/package.json +1 -1
  12. package/src/commit-suggest.js +202 -0
  13. package/src/config-cat.js +88 -0
  14. package/src/docs-check.js +48 -12
  15. package/src/doctor-diagnostics.js +49 -0
  16. package/src/hooks/worktree-guard.js +20 -1
  17. package/src/index.js +547 -21
  18. package/src/init.js +1 -1
  19. package/src/local-register.js +98 -0
  20. package/src/module-impact.js +146 -0
  21. package/src/progress/change-registry.js +43 -4
  22. package/src/progress/stage-machine.js +10 -5
  23. package/src/progress.js +18 -3
  24. package/src/quicklog.js +106 -1
  25. package/src/run/command.js +151 -38
  26. package/src/run/complete-handlers.js +62 -4
  27. package/src/run/complete.js +10 -8
  28. package/src/run/concurrent-detect.js +3 -1
  29. package/src/run/gates.js +86 -4
  30. package/src/run/multi-repo-context.js +3 -1
  31. package/src/run/next.js +128 -0
  32. package/src/run/prompt.js +28 -6
  33. package/src/run/scan-profile.js +35 -23
  34. package/src/run/shared.js +28 -7
  35. package/src/scan-postcheck.js +142 -2
  36. package/src/stage-contract-spec.js +1 -1
  37. package/src/stage-review.js +55 -7
  38. package/src/stages/archive.js +1 -1
  39. package/src/stages/brainstorm.js +26 -12
  40. package/src/stages/doctor.js +2 -4
  41. package/src/stages/execute.js +110 -26
  42. package/src/stages/knowledge.js +21 -5
  43. package/src/stages/plan-postcheck.js +24 -10
  44. package/src/stages/plan.js +14 -9
  45. package/src/stages/quick.js +14 -6
  46. package/src/stages/scan.js +1 -0
  47. package/src/stages/verify.js +7 -49
  48. package/src/sync.js +19 -7
  49. package/src/task-review.js +256 -10
  50. package/src/verify-probes.js +264 -0
  51. package/src/workspace.js +125 -0
  52. package/src/worktree-apply.js +15 -1
  53. package/src/worktree.js +58 -8
  54. package/templates/claude-instruction.md +1 -1
  55. package/templates/prompts/taskcard-rules.md +1 -1
  56. package/templates/prompts/verify-probes.md +13 -47
@@ -45,6 +45,7 @@ export const definition = {
45
45
  7. 检查全局模板:\`ls ~/.sillyspec/templates/\`
46
46
  - 有匹配模板 → 询问是否基于模板
47
47
  - 无相关内容 → 跳过,不输出
48
+ 8. 查询知识库:\`cat {SPEC_ROOT}/knowledge/INDEX.md 2>/dev/null\`(无索引则跳过)——按需求关键词命中条目时读取对应知识文件;known-issues/已知坑要在方案期就规避,不要等 execute 的命中报告才发现
48
49
 
49
50
  ### 模块匹配方法
50
51
  读取 _module-map.yaml 后,根据用户描述的需求关键词,匹配相关模块:
@@ -218,32 +219,44 @@ export const definition = {
218
219
  migratedFrom: ['HTML 原型生成'],
219
220
  requiresWait: true,
220
221
  waitReason: '等待用户确认设计方案',
221
- waitOptions: ['确认', '需要修改', '推翻重来'],
222
- prompt: `展示完整设计方案供用户确认;如适合可视化,顺带生成 HTML 原型。
222
+ waitOptions: ['确认', '需要修改', '补原型', '推翻重来'],
223
+ prompt: `展示完整设计方案供用户确认;涉及 UI 改动时按下方分级决定是否生成 HTML 原型——原型动作必须在暂停之前落定,不能"等确认后再补"。
223
224
 
224
225
  ### 操作
225
226
  1. 简单项目:几句话整体描述
226
227
  2. 复杂项目:按模块/Phase 分段展示,每段 200-300 字
227
228
  3. 展示完整设计方案(不要逐段停顿,一次性展示)
228
229
  4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`)
229
- 5. 暂停等待用户确认或修改意见
230
-
231
- ### HTML 原型生成(适合可视化时)
232
- 判断本次设计是否适合生成 HTML 原型:
233
- - 适合:有 UI 组件/布局/交互流程/状态转换/架构图
234
- - 不适合:纯后端逻辑/配置修改/无可视化意义
235
- 如果适合,生成一个独立的 HTML 文件(内联 CSS + JS),保存到:
236
- \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`
230
+ 5. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
231
+ 6. 暂停等待用户确认或修改意见
232
+
233
+ ### HTML 原型分级(按改动程度,不是"是否碰前端文件")
234
+ 判定对象是**改造后的界面相对现状的变化程度**:
235
+ - **必须生成**(布局/结构/流程级变化):
236
+ - 新增页面、视图、路由
237
+ - 页面布局变化:区域增删、骨架重排、信息层级调整(如单栏改多栏、列表改看板)
238
+ - 新交互流程:多步流程、跨页流转、状态驱动的界面变化
239
+ - 用户提供了截图/设计稿要求按其改造(原型即对照基准)
240
+ - **建议生成**(组件级变化、页面骨架不变):
241
+ - 表单重构、列表列调整、弹窗/抽屉流程等局部组件变化
242
+ - 拿不准时默认生成——宁多勿漏,原型在 execute 阶段可直接对照复用
243
+ - **跳过**(无需生成,但必须声明原因):
244
+ - 文案、颜色、间距、字号、图标等样式微调
245
+ - 纯后端逻辑/CLI/配置/文档,无任何界面变化
246
+
247
+ ### 生成要求
248
+ 生成一个独立的 HTML 文件(内联 CSS + JS),保存到:
249
+ \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`(目录不存在则先创建)
237
250
  - 单文件,浏览器直接打开
238
251
  - 高保真:布局/组件/交互流程按真实效果呈现,后续 execute 阶段可直接对照复用
239
252
  - 与项目代码风格一致:优先复用项目现有技术栈/组件库/设计 token(先查 scan 文档与现有前端代码),不另起风格
240
- 展示给用户确认设计方向。不适合则跳过。
241
253
 
242
254
  ### 铁律 — 必须等待用户确认设计
243
255
  - 展示完整设计方案后必须暂停等用户确认,**不要自己说"设计已充分确认"然后推进**(命令由 CLI 在下方注入)
256
+ - 原型生成或跳过声明必须在暂停**之前**完成——暂停是本步骤最后动作,用户确认后直接进入写 design.md,不会回头补
244
257
 
245
258
  ### 输出
246
- 完整设计方案 + 变更名(+ 原型文件路径,如生成)
259
+ 完整设计方案 + 变更名 + 原型文件路径 或 跳过原因(二选一必填;跳过时写明分级依据,用户可当场否决要求补)
247
260
 
248
261
  ### 注意
249
262
  - 不要一次输出大段文字,按模块/Phase 分段
@@ -333,6 +346,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
333
346
  - design.md 含「自审」字面命中章节(自审 / Self-Review / Self-review)——「生成规范文件」步完成契约 brainstorm.design.self-review 硬要求,写在本步自审段即可
334
347
  - 如存在 decisions.md,design.md 是否引用所有当前版本 D-xxx@vN
335
348
  - 涉及 session/lease/agent_run/daemon/lifecycle 等关键词时,是否含「生命周期契约表」
349
+ - UI 原型核对:文件清单含前端文件(.vue/.tsx/.jsx/.svelte/.html/.css 等)且改动达到「必须生成/建议生成」原型级别(分级见「分段展示设计」步)时,变更目录应已有 \`prototype-*.html\`——没有则本步补生成;确不生成时把跳过原因记入 design.md 风险登记,不得静默缺位
336
350
  5. 缺章节 → 补齐后重检;章节齐全 → 进入下一步(Design Grill 做语义层交叉审查)
337
351
 
338
352
  ### 输出
@@ -73,16 +73,14 @@ const dir = '.sillyspec/changes';
73
73
  const subs = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => { try { return fs.statSync(dir+'/'+f).isDirectory() && f !== 'archive'; } catch { return false; } }) : [];
74
74
  let known;
75
75
  try { const db = new DatabaseSync('.sillyspec/.runtime/sillyspec.db', { readOnly: true }); known = new Set(db.prepare(\"SELECT name FROM changes WHERE status='active'\").all().map(r => r.name)); db.close(); } catch (e) { console.log('⚠️ db 读取失败: ' + e.message + '(跳过孤儿判定,不做任何清理建议)'); known = null; }
76
+ // known 为 null(db 不可读)时只列目录不判定孤儿,后续僵尸检查同样跳过——不守卫会 TypeError 半途崩
76
77
  if (known) subs.forEach(s => {
77
78
  console.log(known.has(s) ? '✅ ' + s + ' — 已关联' : '⚠️ ' + s + ' — 孤儿目录(先 dump 取证再手工归位,勿直接 rm)');
78
79
  });
79
- subs.forEach(s => {
80
- console.log(known.has(s) ? '✅ ' + s + ' — 已关联' : '⚠️ ' + s + ' — 孤儿目录(可清理)');
81
- });
82
80
  // 反向检查(multi-agent-review Q1):DB active 且匹配 quick-<hex> 但无目录 → 僵尸 quick 会话
83
81
  // (quick 收尾未注销,active 行累积污染 listChanges/doctor)。新代码已修收尾,此项清理历史残留。
84
82
  const QUICK_RE = /^quick-[0-9a-f]{8}$/;
85
- const zombieQuick = [...known].filter(n => QUICK_RE.test(n) && !subs.includes(n));
83
+ const zombieQuick = known ? [...known].filter(n => QUICK_RE.test(n) && !subs.includes(n)) : [];
86
84
  if (zombieQuick.length > 0) {
87
85
  console.log('⚠️ 检测到 ' + zombieQuick.length + ' 个僵尸 quick 会话(DB active 但无目录):');
88
86
  zombieQuick.forEach(n => console.log(' - ' + n));
@@ -118,7 +118,8 @@ export function validatePlanForExecute(tasksContent, planContent) {
118
118
  if (count > 1) errors.push(`task id 重复: ${id} 在 tasks.md 出现 ${count} 次`)
119
119
  }
120
120
 
121
- // 检查 3: task id 连续性(从 1 开始)
121
+ // 检查 3: task id 连续性(从 1 开始;ids[0]!==1 时整体跳过 = 兼容旧变更编号不从 1 起,
122
+ // 契约由 test/plan-execute-contract.test.mjs Case 10 钉死,勿改)
122
123
  const ids = allTasks.map(t => t.index).sort((a, b) => a - b)
123
124
  if (ids.length > 0 && ids[0] === 1) {
124
125
  for (let i = 0; i < ids.length; i++) {
@@ -213,6 +214,7 @@ const fixedPrefix = [
213
214
 
214
215
  ### 符号影响面扩展检查
215
216
  11. **符号影响面扫描**(Critical — execute 前必做):
217
+ - **报告骨架勿手写**:先跑 \`sillyspec symbol-impact --change <change-name>\`——CLI 从 tasks.md 生成逐 task \`<!--TODO-->\` 骨架(gate 拦截时也会自动落一份);把每行占位替换为真实结论(**未替换的 TODO 占位会被 gate 拒绝**,骨架不能直接过门)
216
218
  - 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
217
219
  - 对每个修改文件,检查是否涉及以下变更类型:
218
220
  - class 构造函数参数变更(新增/删除/修改参数)
@@ -220,7 +222,7 @@ const fixedPrefix = [
220
222
  - DTO / 类型定义变更
221
223
  - API client 方法签名变更
222
224
  - 函数/方法签名变更(参数增删改)
223
- - 如果涉及上述变更类型,执行调用点搜索:
225
+ - 如果涉及上述变更类型,执行调用点搜索(用于补骨架外的调用点证据,不是生成报告的方式):
224
226
  \`\`\`bash
225
227
  rg "new ClassName\(" src/
226
228
  rg "ClassName\(" src/
@@ -229,7 +231,7 @@ const fixedPrefix = [
229
231
  \`\`\`
230
232
  - 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
231
233
  - **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
232
- - **报告落盘(CLI 硬校验)**:把检查结论写入 \`{SPEC_ROOT}/changes/<change-name>/symbol-impact.md\`,每个 task 一行结论(task-XX: 变更类型 + 受影响调用点 + 是否在范围内;无签名级变更也要显式写「无签名级变更」)。\`--done\` 时 CLI 校验该文件存在且覆盖 plan.md 全部 task,缺失或不覆盖会阻断完成
234
+ - **报告落盘(CLI 硬校验)**:结论写入 \`{SPEC_ROOT}/changes/<change-name>/symbol-impact.md\`,每个 task 一行结论(task-XX: 变更类型 + 受影响调用点 + 是否在范围内;无签名级变更也要显式写「无签名级变更」)。\`--done\` 时 CLI 校验该文件存在且覆盖 plan.md 全部 task,缺失或不覆盖会阻断完成
233
235
  - 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
234
236
 
235
237
  ### 输出
@@ -557,19 +559,20 @@ function resolveTaskRepo(task, changeDir) {
557
559
  }
558
560
 
559
561
  /**
560
- * 把 base_commit 锡点写入 task 卡 frontmatter(W3 task-08,D-010)。
561
- * 派发跨仓 task 前 CLI 实时 git rev-parse HEAD 落盘——锁定子代理起改的 base 锚点,
562
- * 防止同 Wave 多 task 改同一跨仓仓时 HEAD 推进致 diff 范围漂移(design §5.3 约束① / R-01)。
562
+ * 把 commit 锡点(base_commit/head_commit)写入 task 卡 frontmatter(W3 task-08,D-010;
563
+ * head 侧 2026-08-21 审计项③ 补对称)。
563
564
  *
564
- * 策略:幂等写——frontmatter 有 base_commit 行则就地替换,无则紧跟 repo 行后插入(
565
- * 无 frontmatter 时整段创建)。仅改 frontmatter 不动正文。已存在相同值不重复写。
565
+ * 策略:幂等写——frontmatter 有该字段行则就地替换,无则按 D-010 协议顺序插入:
566
+ * head_commit 跟在 base_commit 行后;base_commit 跟在 repo 行后;均无则插 frontmatter
567
+ * 首部。仅改 frontmatter 不动正文。已存在相同值不重复写。
566
568
  *
567
569
  * @param {string} taskFilePath - task-NN.md 绝对路径
568
- * @param {string} baseCommit - 跨仓仓 HEAD sha
570
+ * @param {'base_commit'|'head_commit'} field - 锚点字段名
571
+ * @param {string} commit - commit sha
569
572
  * @returns {boolean} 是否实际写入(false = 文件不存在 / 写失败 / 值未变)
570
573
  */
571
- function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
572
- if (!taskFilePath || !baseCommit) return false
574
+ export function writeCommitAnchorToTaskCard(taskFilePath, field, commit) {
575
+ if (!taskFilePath || !commit || (field !== 'base_commit' && field !== 'head_commit')) return false
573
576
  if (!existsSync(taskFilePath)) return false
574
577
  let content
575
578
  try {
@@ -577,24 +580,29 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
577
580
  } catch {
578
581
  return false
579
582
  }
583
+ // CRLF 归一(坑 base-commit-crlf-frontmatter,plan-postcheck.js 同款):跨仓 task 卡被
584
+ // 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 → 锚点静默不落盘,而 Wave
585
+ // prompt 仍声称「CLI 已落盘」。归一后按 LF 写回(taskcard 生成侧本就强制 LF)。
586
+ content = content.replace(/\r\n?/g, '\n')
580
587
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
581
588
  if (!fmMatch) return false
582
589
  const fm = fmMatch[1]
583
- const baseLineRe = /^base_commit:\s*.*$/m
590
+ const lineRe = new RegExp(`^${field}:\\s.*$`, 'm')
584
591
  let newFm
585
- if (baseLineRe.test(fm)) {
586
- const existing = (fm.match(baseLineRe) || [''])[0]
587
- if (existing === `base_commit: ${baseCommit}`) return false // 值未变,幂等跳过
588
- newFm = fm.replace(baseLineRe, `base_commit: ${baseCommit}`)
592
+ if (lineRe.test(fm)) {
593
+ const existing = (fm.match(lineRe) || [''])[0]
594
+ if (existing === `${field}: ${commit}`) return false // 值未变,幂等跳过
595
+ newFm = fm.replace(lineRe, `${field}: ${commit}`)
589
596
  } else {
590
- // 紧跟 repo 行后插入(与 D-010 协议同源顺序 repo→base_commit→head_commit)
591
- const repoLineRe = /^repo:\s*.*$/m
592
- const anchor = `base_commit: ${baseCommit}\n`
593
- if (repoLineRe.test(fm)) {
594
- newFm = fm.replace(repoLineRe, m => m + '\n' + anchor.trimEnd())
597
+ // 插入锚点(D-010 协议顺序 repo→base_commit→head_commit):head 跟 base 后,
598
+ // base 跟 repo 后,都无则 frontmatter 首字段
599
+ const anchor = `${field}: ${commit}`
600
+ const prevField = field === 'head_commit' ? 'base_commit' : 'repo'
601
+ const prevRe = new RegExp(`^${prevField}:\\s.*$`, 'm')
602
+ if (prevRe.test(fm)) {
603
+ newFm = fm.replace(prevRe, m => m + '\n' + anchor)
595
604
  } else {
596
- // 无 repo 行:插到 frontmatter 首部(frontmatter 首字段)
597
- newFm = anchor.trimEnd() + '\n' + fm
605
+ newFm = anchor + '\n' + fm
598
606
  }
599
607
  }
600
608
  const newContent = content.replace(/^---\n[\s\S]*?\n---/, `---\n${newFm}\n---`)
@@ -607,6 +615,75 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
607
615
  }
608
616
  }
609
617
 
618
+ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
619
+ return writeCommitAnchorToTaskCard(taskFilePath, 'base_commit', baseCommit)
620
+ }
621
+
622
+ /**
623
+ * 跨仓 task head_commit 锡点自动落盘(2026-08-21 agent-手工产出审计项③,D-010 补对称)。
624
+ *
625
+ * base_commit 已在派发时 CLI 落盘,head_commit 此前靠主 agent 按 Wave prompt 指引手跑
626
+ * `rev-parse` 手写(漏抄/抄错直接炸 Task Review Gate 真实性校验)。本函数在 execute
627
+ * --done 时机补齐另一半:扫描 task 卡,跨仓 task(repo≠main)缺 head_commit 的,实时
628
+ * `git -C <跨仓仓根> rev-parse HEAD` 幂等写入(**已存在不覆盖**——agent 手写的精确锚点优先)。
629
+ *
630
+ * best-effort:MultiRepoContext 构造失败 / git 不可达 → 跳过该 task 记 reason,绝不抛。
631
+ *
632
+ * @param {{ changeName: string, cwd: string, specBase?: string, platformOpts?: object }} opts
633
+ * @returns {Promise<{ stamped: number, stampedTasks: string[], skipped: number, reasons: string[] }>}
634
+ */
635
+ export async function stampCrossRepoHeadCommits({ changeName, cwd, specBase, platformOpts = {} }) {
636
+ const base = specBase || platformOpts.specRoot || path.join(cwd, '.sillyspec')
637
+ const reasons = []
638
+ const stampedTasks = []
639
+ if (!changeName) {
640
+ return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 changeName'] }
641
+ }
642
+ const tasksDir = path.join(base, 'changes', changeName, 'tasks')
643
+ if (!existsSync(tasksDir)) {
644
+ return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 tasks/ 目录(单 task 变更,无跨仓锡点)'] }
645
+ }
646
+ let ctx = null
647
+ try {
648
+ const { getOrCreateMultiRepoContext } = await import('../run/shared.js')
649
+ ctx = await getOrCreateMultiRepoContext({ cwd, changeName, platformOpts })
650
+ } catch (e) {
651
+ return { stamped: 0, stampedTasks, skipped: 0, reasons: [`MultiRepoContext 构造失败: ${e.message}`] }
652
+ }
653
+ let stamped = 0
654
+ let skipped = 0
655
+ for (const f of readdirSync(tasksDir).filter(n => /^task-\d+\.md$/.test(n)).sort()) {
656
+ const taskFile = path.join(tasksDir, f)
657
+ let content
658
+ try {
659
+ content = readFileSync(taskFile, 'utf8')
660
+ } catch {
661
+ skipped++
662
+ continue
663
+ }
664
+ if (/^head_commit:/m.test(content)) continue // 已有(手写或前次落盘)不覆盖
665
+ const repoKey = parseRepo(content)
666
+ if (!repoKey || repoKey === 'main') continue // 主仓 task 锚 meta,无 head_commit 字段
667
+ const entry = ctx && ctx.resolve ? ctx.resolve(repoKey) : null
668
+ if (!entry || !entry.gitDir) {
669
+ skipped++
670
+ reasons.push(`${f}: repo "${repoKey}" 无法解析 gitDir(local.yaml repos 未注册?)`)
671
+ continue
672
+ }
673
+ const head = gitQuiet(entry.gitDir, ['rev-parse', 'HEAD'])
674
+ if (!head) {
675
+ skipped++
676
+ reasons.push(`${f}: git rev-parse HEAD 失败(${entry.gitDir})`)
677
+ continue
678
+ }
679
+ if (writeCommitAnchorToTaskCard(taskFile, 'head_commit', head)) {
680
+ stamped++
681
+ stampedTasks.push(f.replace(/\.md$/, ''))
682
+ }
683
+ }
684
+ return { stamped, stampedTasks, skipped, reasons }
685
+ }
686
+
610
687
  /**
611
688
  * 为 Wave 生成 prompt(强制子代理执行)
612
689
  *
@@ -763,9 +840,9 @@ ${crossLines}
763
840
  **派发跨仓 task 前(base 锡点,CLI 已在 prompt 构造时落盘):**
764
841
  - 跨仓 task 卡 frontmatter 的 \`base_commit\` 已由 CLI 实时 \`git -C <跨仓仓根> rev-parse HEAD\` 锁定(base 锡点,约束①)。子代理在此 HEAD 上改+commit,不受同 Wave 其他 task 推进 HEAD 影响。
765
842
 
766
- **回收跨仓 task review 前(head 锡点,你必须在子代理完成后落盘):**
767
- - 跨仓 task 子代理完成 commit 后、写 review.json / 勾选 checkbox **之前**,你必须运行 \`git -C <跨仓仓根> rev-parse HEAD\` 把结果写入该 task 卡 frontmatter \`head_commit:\` 字段(与 \`base_commit:\` 同源 frontmatter 锡点写入)。
768
- - review.json 的 \`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`(**非瞬时 HEAD**),锁 diff 范围不漂移。
843
+ **回收跨仓 task(head 锡点,CLI 自动落盘,勿手写):**
844
+ - 子代理完成 commit 后,正常写 review.json(verdict/notes)并勾选 checkbox 即可。execute \`--done\` 时 CLI 自动 \`git -C <跨仓仓根> rev-parse HEAD\` 写入该 task 卡 \`head_commit:\`(幂等,已存在不覆盖——你若手写了精确锚点则以你的为准)。
845
+ - review.json 的 mechanics 字段(\`base\`/\`head\`/\`changedFiles\`)无需手算:\`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`,写完跑 \`sillyspec backfill-reviews --change <变更名> --adopt\` 一键代填(verdict 保留)。
769
846
 
770
847
  **跨仓 task 子代理 prompt 必须注入:**
771
848
  > 该 task 改的是 \`<repo>\` 仓,workdir=\`<跨仓仓根>\`。**直接在该仓主干工作区改+commit(git add + git commit 到该仓主干),不经主仓 worktree、不建分支。** commit 到该仓主干即落盘,apply 阶段对跨仓 task 为 no-op(design §5.4 G1)。
@@ -958,6 +1035,13 @@ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-X
958
1035
  - 无 _module-map.yaml 时跳过模块匹配,仅按文件清单更新 unmapped 部分
959
1036
  这是可选更新(不阻断 execute 完成),但保持 module-impact 与实际变更一致利于 verify 核对与 archive 终审。
960
1037
 
1038
+ ### 知识沉淀(主代理在本 Wave 收尾,与 module-impact.md 同批)
1039
+ 本 Wave 各 task 子代理报告里若提到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节),由你在 Wave 收尾统一追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
1040
+ - 条目格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源(task-XX)
1041
+ - 不由各 task 子代理分别写(并行改同一文件会互相覆盖),只由主代理在 Wave 收尾统一追加——与 module-impact.md 同一收尾节奏
1042
+ - 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写;拿不准时不写,宁缺毋滥
1043
+ - execute 末步「知识库审阅」会检查待确认条目并提示用户归类
1044
+
961
1045
  ### 完成后
962
1046
  1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
963
1047
  - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
@@ -232,11 +232,12 @@ export async function cmdValidate(dir, args, opts = {}) {
232
232
  }
233
233
  }
234
234
 
235
- // 4. uncategorized.md 条目数(多了提示需清理)
235
+ // 4. uncategorized.md 条目数(多了提示需清理)。条目行契约是 `## <qlId> | <标题>`
236
+ //(quick 自动候选与手工条目同格式),h3 形态一并兼容;原 `^###` 只数 h3 恒为 0
236
237
  const uncategorizedPath = join(knowledgeDir, 'uncategorized.md')
237
238
  if (existsSync(uncategorizedPath)) {
238
239
  const content = readFileSync(uncategorizedPath, 'utf8')
239
- const entryCount = (content.match(/^###\s+/gm) || []).length
240
+ const entryCount = (content.match(/^#{2,3}\s+\S/gm) || []).length
240
241
  if (entryCount >= 10) {
241
242
  warnings.push({
242
243
  code: 'too_many_uncategorized',
@@ -276,10 +277,25 @@ export async function cmdRefresh(dir, args, opts = {}) {
276
277
  // refresh 只写 generated/ 区
277
278
  mkdirSync(generatedDir, { recursive: true })
278
279
 
279
- // 扫描 scan 文档
280
+ // 扫描 scan 文档。项目名用 DB 注册名优先(坑 knowledge-basename-project-mismatch):
281
+ // scan 写侧 docs/<project> 的 project 来自 dbProjectName || basename(cwd),注册名 ≠ 目录名
282
+ // (projects yaml name/path 分离、平台模式 specRoot≠sourceRoot)时,用 basename(dir) 拼
283
+ // scanDir 永远找不到 → refresh 恒报 scan_docs_missing。DB 读失败回退 basename(与写侧
284
+ // default 同口径)。查不到注册名对应目录时再回退 basename 试一次(双形态兼容)。
280
285
  const base = opts.specDir || join(dir, '.sillyspec')
281
- const projectName = basename(dir)
282
- const scanDir = join(base, 'docs', projectName, 'scan')
286
+ let projectName = null
287
+ try {
288
+ const { DatabaseSync } = await import('node:sqlite')
289
+ const db = new DatabaseSync(join(base, '.runtime', 'sillyspec.db'), { readOnly: true })
290
+ const row = db.prepare('SELECT name FROM project LIMIT 1').get()
291
+ db.close()
292
+ if (row && row.name) projectName = row.name
293
+ } catch { /* db 不可读 → 走 basename 兜底 */ }
294
+ if (!projectName) projectName = basename(dir)
295
+ let scanDir = join(base, 'docs', projectName, 'scan')
296
+ if (!existsSync(scanDir) && projectName !== basename(dir)) {
297
+ scanDir = join(base, 'docs', basename(dir), 'scan')
298
+ }
283
299
 
284
300
  if (!existsSync(scanDir)) {
285
301
  output(false, {}, { code: 'scan_docs_missing', path: scanDir })
@@ -348,7 +348,10 @@ function parseTaskWavesFromPlan(planPath) {
348
348
  // (旧变更读侧兼容——新格式由 validatePlanForExecute 拦旧形态,此处不重复把关)
349
349
  const tm = line.match(/^[-*]\s+(?:\[[ x]\]\s*)?(task-\d+)\s*[::\s]*$/i)
350
350
  || line.match(/^[-*]\s*\[[ x]\]\s*(task-\d+)\b/i)
351
- if (tm && currentWave !== null) map.set(tm[1], currentWave)
351
+ // id 归一补零(与卡片/注册表 task-01 口径一致,坑 wave-ref-unpadded):plan.md 写
352
+ // `- task-1` 时裸用捕获原文做 key,永远匹配不到卡片 id task-01 → 被判「未列入任何
353
+ // Wave」,同 Wave 共享文件检测静默跳过。execute.js parseWavesFromPlan 已补零,对齐。
354
+ if (tm && currentWave !== null) map.set(String(tm[1]).replace(/^task-(\d+)$/i, (_, n) => `task-${n.padStart(2, '0')}`), currentWave)
352
355
  }
353
356
  return map
354
357
  }
@@ -614,16 +617,19 @@ export function parseLocalYamlModules(yamlText) {
614
617
  const modules = {}
615
618
  for (let i = startIdx + 1; i < lines.length; i++) {
616
619
  const line = lines[i]
617
- const entry = line.match(/^ ([A-Za-z0-9_.\-]+):\s*(.*)$/)
620
+ const entry = line.match(/^([ \t]+)([A-Za-z0-9_.\-]+):\s*(.*)$/)
618
621
  if (!entry) {
619
622
  // 遇到新的顶层 key(行首非空格且非注释)→ modules 块结束
620
623
  if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('#') && line.trim() !== '') break
621
624
  continue
622
625
  }
623
- const rest = entry[2] || ''
626
+ // 条目缩进放宽为「任意非零空白」(坑 modules-indent-hardcoded):此前硬编码恰 2 空格,
627
+ // 4 空格/Tab 缩进的条目不匹配且不触发块结束 → 被静默跳过 → modules 丢失 →
628
+ // validateScriptCommands 退化为只查根 package.json,monorepo 子包命令被误报不存在。
629
+ const rest = entry[3] || ''
624
630
  // path 值:带引号或不带引号(flow mapping),取第一个匹配
625
631
  const pathMatch = rest.match(/path:\s*"([^"]+)"/) || rest.match(/path:\s*([^,\s}]+)/)
626
- if (pathMatch) modules[entry[1]] = { path: pathMatch[1] }
632
+ if (pathMatch) modules[entry[2]] = { path: pathMatch[1] }
627
633
  }
628
634
  return Object.keys(modules).length > 0 ? modules : null
629
635
  }
@@ -669,9 +675,14 @@ export function validateTaskCommands(changeDir, projectRoot, modules = null) {
669
675
  continue // 非法 YAML 由 feasibility 处理,不重复
670
676
  }
671
677
 
672
- // 合并 verify + implementation 文本(命令可能出现在任一字段)
673
- const verifyText = typeof fm.verify === 'string' ? fm.verify : ''
674
- const implText = typeof fm.implementation === 'string' ? fm.implementation : ''
678
+ // 合并 verify + implementation 文本(命令可能出现在任一字段)。
679
+ // 字段两形态都收(坑 taskcard-block-list-noop):规范格式(taskcard.js 骨架 / plan 模板)的
680
+ // verify/implementation 是 YAML 块列表 → js-yaml 解析为数组;此前只收 string,规范卡片
681
+ // 恒落空 → 「命令存在性校验」(hard-error)对合规 TaskCard 整体 no-op,只有违规写成
682
+ // 纯标量的卡片才被校验。数组按行 join 回文本,与标量同管道。
683
+ const toCmdText = (v) => Array.isArray(v) ? v.map(String).join('\n') : (typeof v === 'string' ? v : '')
684
+ const verifyText = toCmdText(fm.verify)
685
+ const implText = toCmdText(fm.implementation)
675
686
  const text = `${verifyText}\n${implText}`
676
687
  if (!text.trim()) continue
677
688
 
@@ -1047,8 +1058,10 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
1047
1058
  if (allTaskIds.length > 0) {
1048
1059
  const nums = allTaskIds.map(id => {
1049
1060
  const m = id.match(/task-(\d+)/i)
1050
- return m ? parseInt(m[1]) : null
1061
+ return m ? parseInt(m[1], 10) : null
1051
1062
  }).filter(n => n !== null).sort((a, b) => a - b)
1063
+ // nums[0]!==1 时跳过 = 兼容旧变更编号不从 1 起(契约同 validatePlanForExecute 检查 3,
1064
+ // test/plan-execute-contract.test.mjs Case 10 钉死,勿改)
1052
1065
  if (nums.length > 0 && nums[0] === 1) {
1053
1066
  for (let i = 0; i < nums.length; i++) {
1054
1067
  if (nums[i] !== i + 1) {
@@ -1280,11 +1293,12 @@ export async function executePlanPostcheck(context) {
1280
1293
  currentWaveTasks = null
1281
1294
  continue
1282
1295
  }
1283
- // 2026-08-20-task-truth-unify:Wave 段下纯 ID 引用行(- task-NN);旧 checkbox 行同收
1296
+ // 2026-08-20-task-truth-unify:Wave 段下纯 ID 引用行(- task-NN);旧 checkbox 行同收。
1297
+ // 编号补零对齐卡片口径(坑 wave-ref-unpadded,同 parseTaskWavesFromPlan)
1284
1298
  const tm = line.match(/^[-*]\s+(?:\[[ x]\]\s*)?task-(\d+)\s*[::\s]*$/i)
1285
1299
  || line.match(/^[-*]\s*\[[ x]\]\s*task-(\d+)/i)
1286
1300
  if (tm && currentWaveTasks) {
1287
- currentWaveTasks.push(`task-${tm[1]}`)
1301
+ currentWaveTasks.push(`task-${String(tm[1]).padStart(2, '0')}`)
1288
1302
  }
1289
1303
  }
1290
1304
  if (currentWaveTasks) existingWaves.push(currentWaveTasks)
@@ -564,7 +564,9 @@ export const fixedSuffix = [] // postcheck 是动态生成的(需要 changeDir
564
564
  */
565
565
  function parseTaskCount(registryContent) {
566
566
  if (!registryContent || typeof registryContent !== 'string') return 0
567
- const matches = registryContent.match(/^[-*]\s*\[[ x]\]\s*task-\d+/gm)
567
+ // [ xX] + i(坑 taskcount-uppercase-x):parseTaskNames / gates.js extractTaskIdsFromRegistry
568
+ // 均收大写 [X],此处漏收 → 全大写勾选的注册表计数 0 → 误走「无任务」分支丢 coordinator 步
569
+ const matches = registryContent.match(/^[-*]\s*\[[ xX]\]\s*task-\d+/gim)
568
570
  return matches ? matches.length : 0
569
571
  }
570
572
 
@@ -624,15 +626,18 @@ export function buildPlanSteps(changeDir = null, planContent = null) {
624
626
  return [...fixedPrefix, ...postcheck]
625
627
  }
626
628
 
627
- // 解析任务名
629
+ // 解析任务名(源与计数同口径,坑 plan-tasknames-empty-under-registry-contract):新契约
630
+ // (2026-08-20-task-truth-unify)下 plan.md Wave 段是纯 ID 引用行(无 checkbox 无任务名),
631
+ // 此前只从 planContent/plan.md 解析 → taskNames 恒空 → coordinator 步骤任务清单空白,
632
+ // 主 agent 只能自行读 tasks.md 兜底易漏派。tasks.md 优先(与 readTaskRegistryContent 同序)。
628
633
  let taskNames = []
629
- if (planContent) {
630
- taskNames = parseTaskNames(planContent)
631
- } else if (changeDir) {
632
- const planFile = path.join(changeDir, 'plan.md')
633
- if (existsSync(planFile)) {
634
- taskNames = parseTaskNames(readFileSync(planFile, 'utf8'))
635
- }
634
+ let namesSource = planContent || null
635
+ if (changeDir) {
636
+ const registryContent = readTaskRegistryContent(changeDir)
637
+ if (registryContent != null) namesSource = registryContent
638
+ }
639
+ if (namesSource) {
640
+ taskNames = parseTaskNames(namesSource)
636
641
  }
637
642
 
638
643
  // 生成协调器步骤(TaskCard 生成)+ postcheck
@@ -81,7 +81,14 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
81
81
  - 本步骤是最后一步,完成后 quick 会话即结束(--change 传递见首步 sessionId 说明)
82
82
 
83
83
  ### ⚠️ 结果摘要模板(必填,CLI 会校验结构)
84
- \`--output\` 是 QUICKLOG「结果:」归档的唯一来源,**必须按此结构给全四项**(逐项一句话,不可用「见前述」替代):
84
+
85
+ 🔧 **推荐四参数形式**(CLI 自动合成结构化模板,杜绝嵌套冒号/缺字段/截断规则踩坑):
86
+
87
+ \`\`\`
88
+ sillyspec run quick --done --change <id> --req "一句话语义化短标题(写「改了什么」)" --cause "为什么这样改(纯新增/配置/样式写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"
89
+ \`\`\`
90
+
91
+ 兼容旧形式 \`--output "需求:… 根因:… 方案:… 结果:…"\`(**必须按此结构给全四项**,逐项一句话,不可用「见前述」替代;缺任一项 \`--done\` 会被拒并提示缺失字段,补全后重跑即可不丢进度):
85
92
 
86
93
  \`\`\`
87
94
  需求:一句话语义化短标题(写「改了什么」)
@@ -90,15 +97,13 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
90
97
  结果:验证情况(测试数 / lint / typecheck / 部署状态)
91
98
  \`\`\`
92
99
 
93
- ⚠️ **「需求:」就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。
94
-
95
- 缺任一项,\`--done\` 会被拒并提示缺失字段,补全后重跑即可(不丢进度)。
100
+ ⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。旧形式正文内避免嵌套全角冒号(标签后紧接嵌套冒号会被拆分判定缺字段)——这正是四参数形式消灭的事故面。
96
101
 
97
102
  ### 📎 文件括注 --file-notes(可选,省事后手改文件行)
98
103
  若希望 QUICKLOG「文件:」行直接落盘为多行带括注(而非单行逗号路径),\`--done\` 时加 \`--file-notes\`,CLI 落盘为多行 \`- path(括注)\`,免去事后手改:
99
104
 
100
105
  \`\`\`
101
- sillyspec run quick --done --change <id> --output "需求:… 根因:… 方案:… 结果:…" --file-notes "src/a.js::登录端点串限流 || src/b.js::新建 INCR 计数"
106
+ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "…" --result "…" --file-notes "src/a.js::登录端点串限流 || src/b.js::新建 INCR 计数"
102
107
  \`\`\`
103
108
 
104
109
  格式:\`path::括注\` 一条,\`||\` 分隔多条(括注可省略只留 \`path\`)。不传则「文件:」行回填审计到的实际改动文件单行(仍可事后手改)。
@@ -116,7 +121,10 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
116
121
  - 禁止使用 \`git add -A\`
117
122
  - 不要暂存 quick 开始前就已存在的无关改动
118
123
  3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动建/翻骨架——**QUICKLOG 核对按需**(见下方「QUICKLOG 正文核对」;文件行可用 --file-notes 免手改)
119
- 4. 如果发现项目特有的坑,追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`
124
+ 4. 只有遇到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节)时,才追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
125
+ - 格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源 \`(<quicklog-id>)\`
126
+ - 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写
127
+ - 拿不准时不写,宁缺毋滥
120
128
  5. 任务比预期复杂 → 建议用完整流程
121
129
 
122
130
  ### 模块文档同步
@@ -154,6 +154,7 @@ export const definition = {
154
154
  generator: sillyspec-scan
155
155
  ---
156
156
  \`\`\`
157
+ (\`<git-head-short>\` / \`<now-iso-datetime>\` 两占位符由 CLI 注入 prompt 时自动替换为当前 HEAD 短哈希与时间——照 prompt 里已替换的真实值抄写,勿自跑 git rev-parse;漏 header 事后 \`sillyspec scan-fix-headers\` 一键补 author/created_at)
157
158
  - 覆盖已有 scan 文档前先读取旧 frontmatter;如果旧文档的 \`source_commit\` 与当前 HEAD 不一致,或旧文档 \`updated_at\` 晚于本次 scan 开始时间,不要覆盖。
158
159
  - 如果用户明确传入 \`--force-rescan\`,允许覆盖,但仍需写入新的 \`source_commit\` 和 \`updated_at\`。
159
160
 
@@ -197,57 +197,15 @@ export const definition = {
197
197
  你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
198
198
  - **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
199
199
 
200
- 3. 生成 verify-result.md 文件,保存到 \`.sillyspec/changes/<change-name>/verify-result.md\`
201
- 4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)
200
+ 3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成九章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`.sillyspec/changes/<change-name>/verify-result.md\`。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
201
+ 4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论必须写明 PASS/FAIL 字样,留「待填」会被 gate 判不过**
202
202
  5. **核对 module-impact.md**(若 changes/<change>/module-impact.md 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
203
203
 
204
- ### verify-result.md 格式
205
- \`\`\`markdown
206
- # 验证报告(Verify Result)
207
-
208
- ## 结论
209
- PASS / PASS WITH NOTES / FAIL
210
-
211
- ## 任务完成度
212
- (逐项检查任务的结果)
213
-
214
- ## 设计一致性
215
- (对照 design.md 的检查结果)
216
-
217
- ## 探针结果
218
- - 未实现标记扫描:...
219
- - 关键词覆盖:...
220
- - 测试覆盖(含断言有效性抽查):...
221
- - 决策追踪覆盖:...
222
- - API 契约对账:...
223
- - 代码删除对账:声明修改/新增却被整文件删除 = 高风险;清单未列出的删除 = 未声明(CLI 会用 git diff --name-status HEAD advisory 复核,仅 warn 不阻断——是否 FAIL blocker 由你诚实判定)
224
-
225
- ## 决策追踪矩阵(如存在 decisions.md)
226
- | 决策 ID | FR | Task | Evidence | 状态 |
227
- |---|---|---|---|---|
228
- | D-001@v1 | FR-01 | task-01 | test/file/path | PASS/PARTIAL/MISSING |
229
-
230
- ## 测试结果
231
- (测试套件执行结果)
232
-
233
- ## 技术债务
234
- (TODO/FIXME/HACK 统计)
235
-
236
- ## 变更风险等级
237
- (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
238
-
239
- ## Runtime Evidence(integration-critical / deployment-critical 必填;自报告,CLI 仅校验字面存在、不独立核验,须真实执行过)
240
- 按本次变更实际触碰的运行时组件填写(模板是通用形态——长驻进程 / 服务端点 / 生命周期状态机的证据结构;未涉及的行写「不涉及」并注明原因,勿堆砌无关关键词):
241
- - 长驻进程/服务 启动命令:
242
- - 服务地址(本变更触碰的端点):
243
- - 触发核心路径的请求/命令(附关键响应):
244
- - 进程日志关键片段(证明走了新路径、无降级/回退分支):
245
- - 生命周期终态断言(本变更涉及的状态机:初始态 → 运行态 → 终态,逐个确认到达):
246
- - 失败模式排除(针对本变更声明的风险,逐条说明为何未触发):
247
-
248
- ## 代码审查
249
- (问题列表 + 总体评价)
250
- \`\`\`
204
+ ### verify-result.md 章节结构(骨架已含,占位替换即可)
205
+
206
+ 结论(PASS / PASS WITH NOTES / FAIL)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
207
+
208
+ **Runtime Evidence 行结构**(integration/deployment-critical 必填;按实际触碰的运行时组件写,未涉及的行写「不涉及」勿堆关键词):长驻进程启动命令 / 触碰的服务端点 / 触发核心路径的请求(附关键响应)/ 进程日志关键片段(证明走了新路径)/ 生命周期终态断言(初始态→运行态→终态)/ 失败模式排除(逐条说明为何未触发)。
251
209
 
252
210
  ### 输出
253
211
  verify-result.md 路径 + 验证报告摘要 + 下一步命令