sillyspec 3.32.9 → 3.33.0

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 (176) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  2. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  3. package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
  4. package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  6. package/.github/workflows/ci.yml +58 -0
  7. package/.husky/pre-push +7 -6
  8. package/AGENTS.md +1 -1
  9. package/assets/command-cards/flow.md +1 -1
  10. package/eval/eval.config.json +3 -3
  11. package/eval/lib/config.mjs +6 -3
  12. package/eval/lib/util.mjs +16 -3
  13. package/eval/lib/workdir.mjs +4 -1
  14. package/eval/results/eval.db +0 -0
  15. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
  16. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
  17. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
  18. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
  19. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
  20. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
  21. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
  22. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
  23. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
  24. package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
  25. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
  26. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
  27. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
  28. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
  29. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
  30. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
  31. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
  32. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
  33. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
  34. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
  35. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
  36. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
  37. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
  38. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
  39. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
  40. package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
  41. package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
  42. package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
  43. package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
  44. package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
  45. package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
  46. package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  47. package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
  48. package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
  49. package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
  50. package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
  51. package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
  52. package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
  53. package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
  54. package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
  55. package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
  56. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
  57. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
  58. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
  59. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
  60. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
  61. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
  62. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
  63. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  64. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
  65. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  66. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  67. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
  68. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  69. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
  70. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
  71. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
  72. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
  73. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
  74. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
  75. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  76. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
  77. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  78. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
  79. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  80. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
  81. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
  82. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  83. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
  84. package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
  85. package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
  86. package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
  87. package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
  88. package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
  89. package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
  90. package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
  91. package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
  92. package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
  93. package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  94. package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
  95. package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
  96. package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
  97. package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
  98. package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
  99. package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
  100. package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
  101. package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
  102. package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
  103. package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
  104. package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  105. package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
  106. package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
  107. package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
  108. package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
  109. package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
  110. package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
  111. package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
  112. package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
  113. package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
  114. package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
  115. package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
  116. package/eval/tasks/004-phase-stats/task.json +6 -0
  117. package/eval/tasks/004-phase-stats/verify.mjs +38 -0
  118. package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
  119. package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
  120. package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
  121. package/eval/tasks/005-orders-export/task.json +6 -0
  122. package/eval/tasks/005-orders-export/verify.mjs +28 -0
  123. package/package.json +1 -1
  124. package/quicklog/tmp-fix-review.mjs +21 -0
  125. package/src/ceremony-tier.js +2 -1
  126. package/src/commit-suggest.js +45 -4
  127. package/src/config-schema.js +14 -0
  128. package/src/decision-distill.js +106 -24
  129. package/src/decision-redomain.js +148 -0
  130. package/src/decision-renumber.js +157 -0
  131. package/src/design-facts.js +3 -3
  132. package/src/docs-check.js +30 -15
  133. package/src/docs-debt.js +6 -5
  134. package/src/doctor-diagnostics.js +3 -1
  135. package/src/flow-parity.js +28 -4
  136. package/src/flow.js +76 -13
  137. package/src/forensics.js +236 -0
  138. package/src/fr-index.js +17 -2
  139. package/src/hooks/worktree-guard.js +3 -3
  140. package/src/index.js +41 -6
  141. package/src/knowledge-digest.js +23 -6
  142. package/src/knowledge-graph.js +95 -18
  143. package/src/knowledge-match.js +34 -9
  144. package/src/knowledge-stats.js +12 -13
  145. package/src/knowledge-vector.js +98 -7
  146. package/src/machine-interface.js +26 -0
  147. package/src/module-impact.js +2 -5
  148. package/src/modules.js +31 -0
  149. package/src/quicklog.js +6 -1
  150. package/src/run/archive-distill.js +3 -0
  151. package/src/run/command.js +3 -3
  152. package/src/run/complete-handlers.js +8 -3
  153. package/src/run/complete.js +113 -44
  154. package/src/run/gates.js +100 -103
  155. package/src/run/lint-ownership.js +37 -0
  156. package/src/run/prompt.js +43 -28
  157. package/src/run/stage.js +1 -1
  158. package/src/run/verify-quality-scan.js +10 -1
  159. package/src/sentinel-assertions.js +43 -1
  160. package/src/spec-sync.js +81 -2
  161. package/src/stage-contract.js +26 -3
  162. package/src/stage-review.js +46 -12
  163. package/src/stage-templates.js +2 -0
  164. package/src/stages/brainstorm.js +76 -21
  165. package/src/stages/knowledge.js +56 -5
  166. package/src/stages/plan-postcheck.js +13 -2
  167. package/src/stages/plan.js +19 -0
  168. package/src/sync.js +28 -2
  169. package/src/task-tick.js +12 -0
  170. package/src/test-bindings.js +32 -3
  171. package/src/timeline.js +60 -19
  172. package/src/ui-visual.js +119 -4
  173. package/src/verify-postcheck.js +166 -18
  174. package/src/verify-probes.js +61 -18
  175. package/src/worktree-apply.js +26 -2
  176. package/templates/agents-instruction.md +1 -1
package/src/quicklog.js CHANGED
@@ -124,13 +124,18 @@ export function sanitizeDesc(description) {
124
124
  * 「需求:/动机:」类标签前缀,空白折叠单行,超长截断加 …;空输入返回 ''(调用方兜底
125
125
  * change 名)。agent 在 --input 首行写一句中文概括即成标题(flow start 横幅有指引),
126
126
  * --title 显式覆盖一切推导。
127
+ * 段头行防御(2026-10-15-title-label-skip):过门格式示例曾被读成段头形态——agent 把
128
+ * 「动机与背景:」独立成行照抄,白名单无复合词致整行成标题(进度库 6 变更实证)。两道
129
+ * 防御:白名单补复合词(动机与背景/背景与动机);任意短段头行(≤12 字非冒号内容+行尾
130
+ * 冒号,如「变更背景:」「详细说明:」)整行跳过取次非空行——冒号后无内容不成标题。
127
131
  */
128
132
  export function deriveChangeTitle(input, max = 50) {
129
133
  for (const l of String(input || '').replace(/\r\n/g, '\n').split('\n')) {
130
134
  const t = l.trim()
131
135
  if (!t) continue
132
136
  if (/^成功标准\s*[::]/.test(t)) break
133
- const stripped = t.replace(/^(?:需求|动机|背景|任务|标题)\s*[::]\s*/, '').trim()
137
+ if (/^[^::]{1,12}[::]$/.test(t)) continue
138
+ const stripped = t.replace(/^(?:需求|动机|背景|任务|标题|动机与背景|背景与动机)\s*[::]\s*/, '').trim()
134
139
  if (!stripped) continue
135
140
  const one = stripped.replace(/\s+/g, ' ')
136
141
  return one.length > max ? one.slice(0, max) + '…' : one
@@ -39,6 +39,9 @@ export async function executeArchiveDistill({ cwd, specBase, changeName }) {
39
39
  console.warn(`\n⚠️ rejected 决策缺否决理由/复潮条件(needsWait)——这些条目未写盘,其余条目照常提炼。`)
40
40
  console.warn(` 裁决指引(确认归档 --confirm 前处理):`)
41
41
  console.warn(` ① 补录:把缺失字段补进 ${join(changeDir, 'decisions.md')} 后重跑提炼(幂等)——`)
42
+ console.warn(` 可复制补丁样例(在对应 D-xxx@vN 条目末尾追加两行,字段名与值之间用冒号+空格):`)
43
+ console.warn(` - 否决理由: <一句话核心劣势>`)
44
+ console.warn(` - 复潮条件: <何种信号下可重议本方案>`)
42
45
  console.warn(` node --input-type=module -e "import { distillIntoKnowledge } from '<sillyspec 源码路径>/src/decision-distill.js'; console.log(JSON.stringify(distillIntoKnowledge(${JSON.stringify(changeDir)}, ${JSON.stringify(knowledgeRoot)}, ${JSON.stringify(headHash)}), null, 2))"`)
43
46
  console.warn(` ② 跳过:经用户裁决不入库该条,并在「确认归档」--output 注记跳过原因。`)
44
47
  }
@@ -232,7 +232,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
232
232
  function emitReadonlyEmptyGuide() {
233
233
  console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
234
234
  console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
235
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
235
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
236
236
  console.log('')
237
237
  console.log(' 成功标准:')
238
238
  console.log(' - <可验证标准>"')
@@ -858,7 +858,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
858
858
  quickFallbackUsed = true
859
859
  } else {
860
860
  console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
861
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
861
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
862
862
  console.log('')
863
863
  console.log(' 成功标准:')
864
864
  console.log(' - <可验证标准>"')
@@ -2076,7 +2076,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
2076
2076
  if (classification.mode === 'quick') {
2077
2077
  console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
2078
2078
  console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
2079
- console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
2079
+ console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2080
2080
  console.log('')
2081
2081
  console.log(' 成功标准:')
2082
2082
  console.log(' - <可验证标准>"')
@@ -816,9 +816,14 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
816
816
  const { readWatcherEvents, watcherEventsPath } = await import('../watcher.js')
817
817
  const res = readWatcherEvents({ runtimeRoot, change: changeName })
818
818
  if (!res.exists) return { ok: true, skipped: true, reason: `无事件流(${watcherEventsPath(runtimeRoot, changeName)})` }
819
- const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, readBirthTs } = await import('../timeline.js')
819
+ // 侧车透传(2026-10-14-timeline-ticktime-anchor-scope):destDir 内 task-ticks.jsonl 已随归档包
820
+ // ——烤制面读包内侧车(与 tasks.md 同源同路径形态),精确勾选时刻跨机可读;锚收敛 changeName
821
+ // 进解析(消息不含变更名的提交不锚任务行)。
822
+ const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, parseTickLog, readBirthTs } = await import('../timeline.js')
820
823
  let tasks = null
821
824
  try { tasks = parseTaskLines(readRawImpl(join(destDir, 'tasks.md'), 'utf8')) } catch { /* 任务面缺失照烤(渲染层有降级段) */ }
825
+ let tickLog = []
826
+ try { tickLog = parseTickLog(readRawImpl(join(destDir, 'task-ticks.jsonl'), 'utf8')) } catch { /* 侧车缺失照烤(时刻退推断面) */ }
822
827
  let tier = null
823
828
  try { const m = /^tier:\s*(\S+)/m.exec(readRawImpl(join(destDir, 'flow-state.yaml'), 'utf8')); if (m) tier = m[1] } catch { /* 头脑风暴预段等无 flow-state 按未知 */ }
824
829
  const look = gitLook || ((hash) => {
@@ -829,12 +834,12 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
829
834
  const sep2 = rest.indexOf('\x1f')
830
835
  return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) }
831
836
  })
832
- const anchors = resolveCommitAnchors(res.events, look)
837
+ const anchors = resolveCommitAnchors(res.events, look, changeName)
833
838
  let eventsRaw = null
834
839
  try { eventsRaw = readRawImpl(watcherEventsPath(runtimeRoot, changeName), 'utf8') } catch { /* 副本缺源只烤渲染面 */ }
835
840
  const oversize = eventsRaw != null && Buffer.byteLength(eventsRaw, 'utf8') > eventsCopyMaxBytes
836
841
  // 评审 P3-1 清偿:副本未落盘(读源失败或超帽)时头注记必须显式「未随包」,不虚报在场
837
- const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize })
842
+ const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize, tickLog })
838
843
  writeImpl(join(destDir, 'timeline.md'), timelineMd)
839
844
  const files = ['timeline.md']
840
845
  if (eventsRaw != null && !oversize) {
@@ -493,25 +493,26 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
493
493
  .filter((h) => !_hits.some((x) => x.file === h.file && x.id === h.id && (x.change || '') === (h.change || '')))
494
494
  _hits.push(..._viaImpact)
495
495
  } catch { /* 图构建/键解析 fail-soft——词面命中面不受影响 */ }
496
- // 已回应不重弹:decisions.md 正文已含「命中 id+域文件名」共现(小白鼠回应形态
497
- // 「unmapped.md D-001@v1」)→ 视为已甄别过,静默;同轮查询串仍含其词面属预期。
496
+ // 已回应不重弹:decisions.md 某行同时含「命中 id+域文件名(或命中条目变更名)」(小白鼠
497
+ // 回应形态「unmapped.md D-001@v1」)→ 视为已甄别过,静默。行级共现(2026-10-14-decision-id-p2-hardening
498
+ // 收紧:文件级共现时本变更自身同号 id + 正文常见词 backend/unmapped 撞车会静默吞掉他变更命中)。
498
499
  const _answered = _dm
499
500
  ? (h) => {
500
501
  const _dom = String(h.file || '').replace(/^decisions\//, '').replace(/\.md$/, '')
501
- return _dm.includes(h.id) && _dm.includes(_dom)
502
+ return _dm.split(/\r?\n/).some((line) => line.includes(h.id) && (line.includes(_dom) || (h.change && line.includes(h.change))))
502
503
  }
503
504
  : () => false
504
505
  const _echoHits = _hits.filter((h) => !_answered(h))
505
506
  const _entries = km.matched ? (km.entries || []) : []
506
507
  if (_echoHits.length > 0 || _entries.length > 0) {
507
- const { deathPathNote } = await import('../knowledge-match.js')
508
+ const { deathPathNote, decisionChangeQualifier } = await import('../knowledge-match.js')
508
509
  console.warn(`\n⚠️ [knowledge-gate] 方案/决策知识命中(设计时点防复潮提示,warn 不阻断):`)
509
510
  for (const h of _echoHits.slice(0, 5)) {
510
511
  const _note = h.status === 'rejected'
511
512
  ? ` 否决理由:${h.reason || '(未记录)'}`
512
513
  : h.deathPath ? ` ⚰️死路注记:${deathPathNote(h.reason)}` : ''
513
514
  const _via = h.viaImpact ? ` 🧭impact 可达(键:${h.impactKey})` : ''
514
- console.warn(` - ${h.id} ${h.title}(${h.file})status=${h.status || '?'}${_note}${_via}`)
515
+ console.warn(` - ${h.id} ${h.title}(${h.file}|${decisionChangeQualifier(h.change)})status=${h.status || '?'}${_note}${_via}`)
515
516
  }
516
517
  for (const e of _entries.slice(0, 3)) {
517
518
  const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
@@ -673,40 +674,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
673
674
  // 勾选回归 agent 手动(完成=实现+测试绿+wt-commit 即勾,同 thin 工作单元语义),假勾防线由
674
675
  // detectExecuteBatchFinish 内 checkExecuteCodeEvidence 代码证据核验 + verify 测试对账承担。
675
676
  if (stageName === 'execute' && changeName) {
676
- // 自动勾选(full-autopilot-parity,R21 thin 实证迁移):execute --done 时解析 run 内提交的
677
- // task-NN token,自动勾选 tasks.md 未勾条目——agent 手动勾选是 R19/R20 的行为惯性(tick-loop-nudge
678
- // 提示词无效),机器代勾与 thin 的 flow done 同逻辑。fail-soft。
679
- try {
680
- const _tasksMdPath = join(specBase, 'changes', changeName, 'tasks.md')
681
- if (existsSync(_tasksMdPath)) {
682
- const _runIdFile = join(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
683
- let _tickCommits = ''
684
- try {
685
- if (existsSync(_runIdFile)) {
686
- _tickCommits = gitQuiet(cwd, ['log', '--format=%B%x1e', 'HEAD~20..HEAD']) || ''
687
- }
688
- } catch {}
689
- if (!_tickCommits) _tickCommits = gitQuiet(cwd, ['log', '--format=%B%x1e', '-20']) || ''
690
- if (_tickCommits) {
691
- const _texts = String(_tickCommits).split('\x1e').map(x => x.trim()).filter(Boolean).join('\n')
692
- const _evidenced = new Set()
693
- for (const m of _texts.matchAll(/task-(\d{1,2})/gi)) _evidenced.add(String(Number(m[1])).padStart(2, '0'))
694
- if (_evidenced.size > 0) {
695
- let _md = readFileSync(_tasksMdPath, 'utf8')
696
- let _ticked = 0
697
- for (const nn of _evidenced) {
698
- const re = new RegExp(`^- \\[ \\](.*task-${nn}[:\\s])`, 'm')
699
- if (re.test(_md)) { _md = _md.replace(re, `- [x]$1`); _ticked++ }
700
- }
701
- if (_ticked > 0) {
702
- const { writeAtomicSync } = await import('../fs-atomic.js')
703
- writeAtomicSync(_tasksMdPath, _md)
704
- console.log(`✅ [自动勾选] ${_ticked} 个任务有提交证据但未勾——已机器代勾(full-autopilot-parity)`)
705
- }
706
- }
707
- }
708
- }
709
- } catch { /* auto-tick fail-soft */ }
677
+ // 自动勾选(full-autopilot-parity,R21 thin 实证迁移):代勾判据/实现见
678
+ // autoTickTasksFromCommitSubjects(subject token × allowed_paths 实现事实锚)。
679
+ await autoTickTasksFromCommitSubjects({ changeName, cwd, specBase, platformOpts })
710
680
  const _bf = await detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps })
711
681
  if (_bf.batched && _bf.aligned > 0) {
712
682
  console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
@@ -1532,8 +1502,10 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
1532
1502
  if (isExplicitReviewWrite(r.review)) return true
1533
1503
 
1534
1504
  // W2 task-04 草稿零 diff 守卫(FR-03)
1535
- // ctx 给定且 review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空
1536
- if (ctx && r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
1505
+ // review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空。ctx 缺席(直写/主仓形态
1506
+ // buildDraftContext 返 null)同样保守不勾——原「ctx=null 直接放行」使 cannot_verify 草稿
1507
+ // 无任何实现核验即被代勾(坑 wt-commit-auto-tick-unimplemented-tasks 连带,2026-10-15)。
1508
+ if (r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
1537
1509
  const taskId = r.review.task || 'task-NN'
1538
1510
  const unlockCmd = `sillyspec review write${changeName ? ` --change ${changeName}` : ' --change <变更名>'} --task ${taskId} --spec pass --quality pass --force --changed-files <文件>`
1539
1511
  const changedFiles = r.review.changedFiles || []
@@ -1541,8 +1513,8 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
1541
1513
  console.warn(`⚠️ 草稿 review changedFiles 为空,跳过自动勾选(task=${taskId})——解锁:${unlockCmd}(纯验证任务用 --changed-files "" + --evidence "<验证说明>")`)
1542
1514
  return false
1543
1515
  }
1544
- if (!ctx.gitDir || !ctx.base || !ctx.head) {
1545
- console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff 校验,保守不勾(task=${taskId})——解锁:${unlockCmd}`)
1516
+ if (!ctx || !ctx.gitDir || !ctx.base || !ctx.head) {
1517
+ console.warn(`⚠️ ctx 缺席或不完整(gitDir=${ctx && ctx.gitDir}, base=${ctx && ctx.base}, head=${ctx && ctx.head}),跳过草稿 diff 校验,保守不勾(task=${taskId})——解锁:${unlockCmd}`)
1546
1518
  return false
1547
1519
  }
1548
1520
  // 预取路径:一次全量 diff 文件集内存归属(autoCheckPlanFromReviews/批量路径已 prefetch)
@@ -1572,6 +1544,86 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
1572
1544
 
1573
1545
  return true
1574
1546
  }
1547
+ /**
1548
+ * execute --done 的 full-autopilot 代勾(独立函数供测试直驱;completeStep execute 分支调用)。
1549
+ *
1550
+ * 实现事实锚(坑 wt-commit-auto-tick-unimplemented-tasks,2026-10-15 消费仓两次实证:原按
1551
+ * %B 提交全文提 token——task-01 提交正文提及 task-02/03/04 即全被误勾,批量完成判定与续跑
1552
+ * 跳过双失真):证据源收敛为提交 subject 行(%s)的 task-NN token,且任务卡 allowed_paths
1553
+ * 在场时要求「提及该 task 的提交触及面 × allowed_paths」交叠(parseAllowedPaths +
1554
+ * pathMatches,与草稿归属同口径);无卡面/无 allowed_paths 退化为仅 subject token。无锚不勾。
1555
+ * fail-soft(异常零行为)。
1556
+ * @param {{ changeName: string, cwd: string, specBase: string, platformOpts?: object }} opts
1557
+ */
1558
+ export async function autoTickTasksFromCommitSubjects({ changeName, cwd, specBase, platformOpts = {} }) {
1559
+ try {
1560
+ const _tasksMdPath = join(specBase, 'changes', changeName, 'tasks.md')
1561
+ if (!existsSync(_tasksMdPath)) return { ticked: 0, skippedNoAnchor: 0 }
1562
+ const _runIdFile = join(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
1563
+ let _tickLog = ''
1564
+ try {
1565
+ if (existsSync(_runIdFile)) {
1566
+ _tickLog = gitQuiet(cwd, ['log', '--name-only', '--format=%x1e%s', 'HEAD~20..HEAD'], { trim: false }) || ''
1567
+ }
1568
+ } catch {}
1569
+ if (!_tickLog) _tickLog = gitQuiet(cwd, ['log', '--name-only', '--format=%x1e%s', '-20'], { trim: false }) || ''
1570
+ if (_tickLog) {
1571
+ // 逐提交解析:首行 = subject(token 证据面——正文散文提及不是实现证据),其余行 = 该提交触及文件
1572
+ const _evidenced = new Map()
1573
+ for (const _chunk of String(_tickLog).split('\x1e')) {
1574
+ const _lines = String(_chunk || '').split('\n').map(x => x.trim()).filter(Boolean)
1575
+ if (_lines.length === 0) continue
1576
+ for (const m of _lines[0].matchAll(/task-(\d{1,2})/gi)) {
1577
+ const nn = String(Number(m[1])).padStart(2, '0')
1578
+ if (!_evidenced.has(nn)) _evidenced.set(nn, new Set())
1579
+ for (const f of _lines.slice(1)) _evidenced.get(nn).add(f)
1580
+ }
1581
+ }
1582
+ if (_evidenced.size > 0) {
1583
+ // 任务卡 allowed_paths 索引(卡面读取失败 → 无卡面口径:仅 subject token)
1584
+ const _allowedByTask = new Map()
1585
+ let _pathMatches = null
1586
+ try {
1587
+ const { parseAllowedPaths } = await import('../stages/plan-postcheck.js')
1588
+ ;({ pathMatches: _pathMatches } = await import('../change-list.js'))
1589
+ const _tasksDir = join(specBase, 'changes', changeName, 'tasks')
1590
+ for (const nn of _evidenced.keys()) {
1591
+ const _cardPath = join(_tasksDir, `task-${nn}.md`)
1592
+ if (existsSync(_cardPath)) {
1593
+ const _paths = parseAllowedPaths(readFileSync(_cardPath, 'utf8'))
1594
+ .map(p => String(p).replace(/^NEW:\s*/, '').trim()).filter(Boolean)
1595
+ if (_paths.length > 0) _allowedByTask.set(nn, _paths)
1596
+ }
1597
+ }
1598
+ } catch { /* 卡面读取失败 → 无锚退化为仅 subject token(与无卡面同口径) */ }
1599
+ let _md = readFileSync(_tasksMdPath, 'utf8')
1600
+ let _ticked = 0
1601
+ let _skippedNoAnchor = 0
1602
+ for (const [nn, _files] of _evidenced) {
1603
+ const _allowed = _allowedByTask.get(nn)
1604
+ if (_allowed && _pathMatches && ![..._files].some(f => _allowed.some(p => _pathMatches(f, p)))) {
1605
+ _skippedNoAnchor++
1606
+ continue // subject 提及但触及面与声明面零交叠——无实现事实锚不勾
1607
+ }
1608
+ const re = new RegExp(`^- \\[ \\](.*task-${nn}[:\\s])`, 'm')
1609
+ if (re.test(_md)) { _md = _md.replace(re, `- [x]$1`); _ticked++ }
1610
+ }
1611
+ if (_ticked > 0) {
1612
+ const { writeAtomicSync } = await import('../fs-atomic.js')
1613
+ writeAtomicSync(_tasksMdPath, _md)
1614
+ console.log(`✅ [自动勾选] ${_ticked} 个任务有实现证据(提交 subject token${_allowedByTask.size > 0 ? ' × allowed_paths 交叠' : ''})但未勾——已机器代勾(full-autopilot-parity)`)
1615
+ }
1616
+ if (_skippedNoAnchor > 0) {
1617
+ console.log(`ℹ️ [自动勾选] ${_skippedNoAnchor} 个 task 号有 subject token 但触及面与 allowed_paths 零交叠——不代勾(无实现事实锚),完成时请人工核对勾选`)
1618
+ }
1619
+ return { ticked: _ticked, skippedNoAnchor: _skippedNoAnchor }
1620
+ }
1621
+ }
1622
+ return { ticked: 0, skippedNoAnchor: 0 }
1623
+ } catch { /* auto-tick fail-soft */ }
1624
+ return { ticked: 0, skippedNoAnchor: 0 }
1625
+ }
1626
+
1575
1627
  export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
1576
1628
  if (stageName !== 'execute' || !changeName) {
1577
1629
  return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
@@ -1713,7 +1765,7 @@ export function detectVerifyBatchFinish({ changeName, specBase, steps }) {
1713
1765
  }
1714
1766
  }
1715
1767
 
1716
- async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps }) { if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
1768
+ export async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, platformOpts, steps }) { if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
1717
1769
  try {
1718
1770
  const changeDir = join(specBase, 'changes', changeName)
1719
1771
  const { total: planTotal, checked: planChecked } = pm.readPlanCheckboxStatus(changeDir)
@@ -1747,6 +1799,23 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
1747
1799
  // → 每个 task 进 blockedTasks → 批量完成静默失效
1748
1800
  const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
1749
1801
  const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1802
+ // ── 缺席草稿自动补齐(坑 task-review-retired-archive-mismatch,2026-10-15 归档门口实证)──
1803
+ // Task Review 层退役(2026-09-26)后 execute 批量完成不再产 per-task review.json,但本函数
1804
+ // 下方复核循环与 archive step2 完成度判定(真相源=review.json verdict)仍以它在场为前提——
1805
+ // 正常走完五阶段的变更到归档门口必然全量「review.json 缺失」。退役时 generateTaskReviewDrafts
1806
+ // 只摘了调用点(函数本体与 backfill-reviews 手动通道都在):此处对缺失面重新接线——机械
1807
+ // 幂等(已存在不覆盖人工/子代理 verdict),失败 fail-open 不阻断批量(最坏退回修复前形态,
1808
+ // 下方复核循环照旧拦)。挂在 runId marker 读取之前:run 缺失时生成器建 run + 写 marker,
1809
+ // 下方循环一次读盘即见。
1810
+ try {
1811
+ const { generateTaskReviewDrafts } = await import('../task-review.js')
1812
+ const draftOut = await generateTaskReviewDrafts({ changeName, cwd, platformOpts })
1813
+ if (draftOut && draftOut.generated > 0) {
1814
+ console.log(`📝 [批量完成前置] 自动补 ${draftOut.generated} 份 per-task review 草稿(机械幂等;archive 完成度真相源在场——草稿 verdict=cannot_verify 仍由 verify 兑证/人工复核升级,summarizeTaskCompletion 单列可见)`)
1815
+ }
1816
+ } catch (e) {
1817
+ console.warn(`⚠️ per-task 草稿自动补齐异常(fail-open,批量完成继续走既有复核循环): ${(e && e.message) || e}`)
1818
+ }
1750
1819
  const blockedTasks = []
1751
1820
  const { readReview, isValidExecuteRunId } = await import('../task-review.js')
1752
1821
  // runId marker 对全部 task 恒定:循环外一次读盘(原实现逐 task 重复 readFileSync 同一文件)
package/src/run/gates.js CHANGED
@@ -224,7 +224,7 @@ export function warnMissingUiPrototype(stageName, changeName, currentStepName, s
224
224
  const design = readFileSync(designPath, 'utf8')
225
225
  const feExts = [...new Set((design.match(/\.(vue|svelte|astro|tsx|jsx|css|scss|sass|less|styl|html)\b/gi) || []).map(s => s.replace('.', '').toLowerCase()))]
226
226
  if (feExts.length === 0) return
227
- console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成;已有可复跑 HTML 原型则登记复用:复制进变更目录并命名 prototype-<名称>.html,execute 按此命名注入)`)
227
+ console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成;已有可复跑 HTML 原型则登记复用:复制进变更目录并命名 prototype-<名称>.html,execute 按此命名注入;本仓形态未在 local.yaml ui_prototype 登记的,收口前补登记——form: real-code 或 single-html+豁免 ruling)`)
228
228
  }
229
229
 
230
230
  /**
@@ -958,6 +958,99 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
958
958
  writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'target_files ②类(missing_declared)阻断' })
959
959
  docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'target_files 对账阻断(②类「声明未做」——计划落空,明细见上)' })
960
960
  }
961
+ // ── 探针一致性 / draft 篡改门禁 / ceremony 双跑三门(坑 verify-close-gate-staggered-exposure,
962
+ // 2026-10-15 消费仓四轮 --done 实证:三门原在实测门之后逐个 fail-fast——每轮先付 195s
963
+ // 实测再见一个新文档问题,四轮纯往返约 20 分钟。三门数据面(facts 已于 runValidators 前
964
+ // backfill、verify-result.md 正文/sidecar、ceremony tier 文件、git diff)与实测结果零数据
965
+ // 依赖——照 required-evidence/reconcile 先例前移并入 docGateFailures 收集(R16 聚合一次
966
+ // 全列),失败统一在检查点 A 回滚(进度不丢,修复后重跑 --done)。──
967
+ // 探针一致性(task-03 / ir-stage-p3b D-002@v1 方案A):「#### 探针 N」机械预填段长在 agent
968
+ // 可编辑的正文里,其防篡改基准是正文锚点而非 verify-facts.json(删底稿绕不过防护)。
969
+ // checkProbeConsistency 重跑只读探针与正文锚点对账,分级(D-003):probe1/probe6 不符 =
970
+ // ERROR → 收集阻断(正文预填段疑似被篡改);probe3/probe5 环境敏感维度 + R-06 HEAD 前进
971
+ // 降级 = WARNING → 放行告警(drift);skipped/degraded → 放行提示。
972
+ // 接线形态照 P3a reconcile 先例:envelope + print(ERROR 时 console.error 逐条 mismatch +
973
+ // supportedFixes)+ verify-runs 落盘 + docGateFailures。
974
+ const { checkProbeConsistency } = await import('../verify-postcheck.js') // isStrictChange 复用本块 reconcile 段既有 import
975
+ const probeCheck = checkProbeConsistency({
976
+ // 四参与 reconcile 同源取值:cwd/specBase/changeName 原样透传;runtimeRoot 复用上方
977
+ // reconcileRuntimeRoot(resolveRuntimeRoot(platformOpts, specBase))——checkProbeConsistency
978
+ // 的锚点产物实际都在 changeDir,此参为统一传参签名对齐预留,口径一处定义不二算
979
+ cwd, specBase, changeName,
980
+ runtimeRoot: reconcileRuntimeRoot,
981
+ // IR 严格档(2026-09-07-ir-hardening D-002@v1):created_at ≥ IR_STRICT_SINCE 的变更
982
+ // 探针子节全缺 = ERROR(存量 skip 语义零变化);fail-open(读不到 → false 落豁免)
983
+ strictMode: isStrictChange({ pm, cwd, changeName }),
984
+ })
985
+ const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
986
+ const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
987
+ // 探针7 covered 证据锚点 advisory(坑 probe7-covered-anchor-missing,2026-09-15 复盘:7 行
988
+ // covered 证据缺 file:line 被审查打回——预填说明已载明口径,机器在 --done 时点再拦一道,
989
+ // 省一轮人工往返)。advisory 不阻断;独立模块零环(verify-probes 是多会话冲突面,不碰)。
990
+ try {
991
+ const { checkProbe7AnchorCoverage } = await import('../probe7-anchor-check.js')
992
+ const p7ReportPath = join(specBase, 'changes', changeName, 'verify-result.md')
993
+ if (existsSync(p7ReportPath)) {
994
+ const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
995
+ if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
996
+ console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
997
+ for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
998
+ console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
999
+ }
1000
+ if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
1001
+ }
1002
+ }
1003
+ } catch { /* 锚点校验异常 fail-soft 不影响 gate */ }
1004
+ // 结果落盘(对齐 writeReconcileRunResult 先例):独立文件 probe-consistency-result.json——
1005
+ // 与 reconcile-result.json 同 gate 运行同目录组织(verify-runs/<ts>/),但不并入后者:
1006
+ // reconcile-result.json 的 schema 已被 P3a 消费方锁定,塞探针字段(读改写/扩参)只会引入
1007
+ // 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
1008
+ writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
1009
+ if (probeBlocked) {
1010
+ writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: '探针一致性抽查 error 阻断' })
1011
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: '探针一致性抽查 error 阻断(明细见上)' })
1012
+ }
1013
+ // ── verify 填槽制篡改门禁(r5l 方案3 / 评审护栏#2 承重件,v2 件二)──
1014
+ // verify-probes --init --draft 生成的机器预填段(任务完成度/风险等级/测试结果/决策追踪)带
1015
+ // sha256 指纹标记 + .runtime sidecar 台账;--done 对账:标记被删 / 内容哈希失配 → 收集阻断
1016
+ // (heredoc 整份重写的典型形态——Write 可用与否均免疫,机制修法不做生成侧劝说)。修改机器
1017
+ // 段唯一通道 verify-probes --amend-draft(重锚 + amendment 审计)。无 sidecar(未用 draft /
1018
+ // 存量变更)→ not-applicable 零红;门禁自身异常 fail-soft(对齐探针抽查 degraded 语义)。
1019
+ try {
1020
+ const { checkDraftIntegrity } = await import('../verify-draft.js')
1021
+ const draftCheck = checkDraftIntegrity({
1022
+ mdPath: join(specBase, 'changes', changeName, 'verify-result.md'),
1023
+ changeName, runtimeRoot: reconcileRuntimeRoot,
1024
+ })
1025
+ if (draftCheck.applicable && !draftCheck.ok) {
1026
+ console.error(`\n❌ verify-result.md 机器预填段篡改门禁:${draftCheck.violations.length} 处失配——draft 预填段被整份重写或标记被删/被手工重锚。`)
1027
+ for (const v of draftCheck.violations) console.error(` - ${v}`)
1028
+ console.error(' 修复:还原机器段原文(AGENT 槽——结论枚举/移交项/审查叙述——不受本门限制);确要修改机器段,跑 sillyspec verify-probes --change ' + changeName + ' --amend-draft 留痕重锚后重新 --done。')
1029
+ writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'draft 篡改门禁阻断' })
1030
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'verify-result.md 机器预填段篡改门禁(明细见上)' })
1031
+ }
1032
+ } catch { /* 门禁异常 fail-soft(不是篡改,对齐探针 degraded 放行) */ }
1033
+ // ── ceremony 双跑收口·第一出口(2026-09-18-ceremony-risk-pricing task-04 / FR-03 / D-003;
1034
+ // execute 评审 FAIL 项补接线):声明档(.runtime/ceremony-tier-<change>.json 开跑定价)vs
1035
+ // 实际 diff 重跑 blast+span 的事实档,mismatch(severity=error,懒 agent 低报)汇入收集
1036
+ // 硬 flag 面(照 reconcile/probe 先例检查点 A 统一回滚,修复后重跑 --done 进度不丢)。
1037
+ // 检查/记账/种子收敛在 runCeremonyDualRunCheck 单点(archive 出口 complete-handlers.js
1038
+ // runArchiveCeremonyDualRunExit 同款消费);skipped(档位文件缺失=存量变更零红门禁)/
1039
+ // degraded(git 不可用降级)零噪音放行;整体 fail-soft——检查自身异常降级放行(异常
1040
+ // 不是低报,对齐 archive 出口同款处置)。specBase/runtimeRoot 与四邻检查同源。──
1041
+ try {
1042
+ const { runCeremonyDualRunCheck, printCeremonyDualRunCheck } = await import('../verify-postcheck.js')
1043
+ const ceremonyCheck = await runCeremonyDualRunCheck({
1044
+ cwd, specBase, changeName,
1045
+ runtimeRoot: reconcileRuntimeRoot,
1046
+ })
1047
+ printCeremonyDualRunCheck(ceremonyCheck)
1048
+ if (ceremonyCheck.status === 'mismatch' && ceremonyCheck.severity === 'error') {
1049
+ docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'ceremony 双跑 mismatch(声明档低报,明细见上)' })
1050
+ }
1051
+ } catch (e) {
1052
+ console.warn(`⚠️ Ceremony 双跑对账异常降级放行(不阻断 verify,fail-soft): ${(e && e.message) || e}`)
1053
+ }
961
1054
  }
962
1055
  // ── 收集模式检查点 A(verify):纯文档门全清后才进入实测门(test/lint/parity/probe 保持
963
1056
  // fail-fast 不变)。brainstorm/plan/execute 不走本检查点(其实测面为空,检查点 B 收口)。──
@@ -1304,18 +1397,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1304
1397
  // 快照含基线债文件时恒败,agent 被逼范围外清偿或逃生口)。失败输出提及文件 × 本变更
1305
1398
  // 文件集零交集 → 存量债 advisory 降档放行(债非本变更引入,硬拦只是制造范围外功);
1306
1399
  // 有交集 / 输出无可识别路径(unattributable,纯配置错误类)→ 维持硬拦(保守)。──
1307
- let lintOwnership = null
1308
- try {
1309
- const { triageLintOwnership, resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
1310
- const lintScopeFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
1311
- .map(f => String(f).replace(/\\/g, '/')).filter(f => !f.startsWith('.sillyspec/'))
1312
- lintOwnership = triageLintOwnership({ failureFiles: lintCheck.failureFiles || [], changeFiles: lintScopeFiles })
1313
- } catch { /* 鉴定链异常维持硬拦(保守) */ }
1314
- if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
1315
- console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 完成:')
1316
- console.warn(` 存量债文件:${(lintCheck.failureFiles || []).join('、') || '(见上方输出)'}`)
1317
- console.warn(' 处置建议:单独 quick 机械清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
1318
- } else {
1400
+ // 2026-10-15-lint-triage-path-unify:判定与文案收敛到 ./lint-ownership.js(与 noAI 质量
1401
+ // 扫描路径同源——原两路径分叉见消费仓坑档案 verify-lint-triage-path-divergence.md)。
1402
+ const { lintOwnershipDowngrade } = await import('./lint-ownership.js')
1403
+ const lintAdvisoryDowngraded = await lintOwnershipDowngrade({ lintCheck, cwd, changeName, specBase })
1404
+ if (!lintAdvisoryDowngraded) {
1319
1405
  console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
1320
1406
  if (usedSnapInfo) {
1321
1407
  try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
@@ -1339,7 +1425,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1339
1425
  }
1340
1426
  console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
1341
1427
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-lint' })
1342
- } // else(pre-existing 降档分支)结束——维持硬拦分支的 rollback 语义不变
1428
+ } // 硬拦分支结束(降档放行时直接落到后续 parity/删除探针——rollback 语义不变)
1343
1429
  }
1344
1430
  // 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
1345
1431
  // 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
@@ -1351,95 +1437,6 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1351
1437
  const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
1352
1438
  const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
1353
1439
  printVerifyDeletionCheck(deletionCheck)
1354
- // ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
1355
- // 「#### 探针 N」机械预填段(verify-probes --init 生成)长在 agent 可编辑的正文里,其防篡改
1356
- // 基准是正文锚点而非 verify-facts.json(删底稿绕不过防护)。checkProbeConsistency 重跑只读
1357
- // 探针与正文锚点对账,分级(D-003):probe1/probe6 不符 = ERROR → 阻断回滚(正文预填段疑似
1358
- // 被篡改);probe3/probe5 环境敏感维度 + R-06 HEAD 前进降级 = WARNING → 放行告警(drift);
1359
- // skipped(存量旧格式报告/无 changeName)/degraded(重跑异常 fail-soft)→ 放行提示。
1360
- // 接线形态照上方 P3a reconcile 先例:envelope + print(ERROR 时 console.error 逐条 mismatch +
1361
- // supportedFixes)+ verify-runs 落盘 + rollbackCompletionAndReturn。
1362
- const { checkProbeConsistency, isStrictChange } = await import('../verify-postcheck.js')
1363
- const probeCheck = checkProbeConsistency({
1364
- // 四参与 reconcile 同源取值:cwd/specBase/changeName 原样透传;runtimeRoot 复用上方
1365
- // reconcileRuntimeRoot(resolveRuntimeRoot(platformOpts, specBase))——checkProbeConsistency
1366
- // 的锚点产物实际都在 changeDir,此参为统一传参签名对齐预留,口径一处定义不二算
1367
- cwd, specBase, changeName,
1368
- runtimeRoot: reconcileRuntimeRoot,
1369
- // IR 严格档(2026-09-07-ir-hardening D-002@v1):created_at ≥ IR_STRICT_SINCE 的变更
1370
- // 探针子节全缺 = ERROR(存量 skip 语义零变化);fail-open(读不到 → false 落豁免)
1371
- strictMode: isStrictChange({ pm, cwd, changeName }),
1372
- })
1373
- const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
1374
- const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
1375
- // 探针7 covered 证据锚点 advisory(坑 probe7-covered-anchor-missing,2026-09-15 复盘:7 行
1376
- // covered 证据缺 file:line 被审查打回——预填说明已载明口径,机器在 --done 时点再拦一道,
1377
- // 省一轮人工往返)。advisory 不阻断;独立模块零环(verify-probes 是多会话冲突面,不碰)。
1378
- try {
1379
- const { checkProbe7AnchorCoverage } = await import('../probe7-anchor-check.js')
1380
- const p7ReportPath = join(specBase, 'changes', changeName, 'verify-result.md')
1381
- if (existsSync(p7ReportPath)) {
1382
- const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
1383
- if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
1384
- console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
1385
- for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
1386
- console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
1387
- }
1388
- if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
1389
- }
1390
- }
1391
- } catch { /* 锚点校验异常 fail-soft 不影响 gate */ }
1392
- // 结果落盘(对齐 writeReconcileRunResult 先例):独立文件 probe-consistency-result.json——
1393
- // 与 reconcile-result.json 同 gate 运行同目录组织(verify-runs/<ts>/),但不并入后者:
1394
- // reconcile-result.json 的 schema 已被 P3a 消费方锁定,塞探针字段(读改写/扩参)只会引入
1395
- // 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
1396
- writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
1397
- if (probeBlocked) {
1398
- writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: '探针一致性抽查 error 阻断' })
1399
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1400
- }
1401
- // ── verify 填槽制篡改门禁(r5l 方案3 / 评审护栏#2 承重件,v2 件二)──
1402
- // verify-probes --init --draft 生成的机器预填段(任务完成度/风险等级/测试结果/决策追踪)带
1403
- // sha256 指纹标记 + .runtime sidecar 台账;--done 对账:标记被删 / 内容哈希失配 → 阻断回滚
1404
- // (heredoc 整份重写的典型形态——Write 可用与否均免疫,机制修法不做生成侧劝说)。修改机器
1405
- // 段唯一通道 verify-probes --amend-draft(重锚 + amendment 审计)。无 sidecar(未用 draft /
1406
- // 存量变更)→ not-applicable 零红;门禁自身异常 fail-soft(对齐探针抽查 degraded 语义)。
1407
- try {
1408
- const { checkDraftIntegrity } = await import('../verify-draft.js')
1409
- const draftCheck = checkDraftIntegrity({
1410
- mdPath: join(specBase, 'changes', changeName, 'verify-result.md'),
1411
- changeName, runtimeRoot: reconcileRuntimeRoot,
1412
- })
1413
- if (draftCheck.applicable && !draftCheck.ok) {
1414
- console.error(`\n❌ verify-result.md 机器预填段篡改门禁:${draftCheck.violations.length} 处失配——draft 预填段被整份重写或标记被删/被手工重锚。`)
1415
- for (const v of draftCheck.violations) console.error(` - ${v}`)
1416
- console.error(' 修复:还原机器段原文(AGENT 槽——结论枚举/移交项/审查叙述——不受本门限制);确要修改机器段,跑 sillyspec verify-probes --change ' + changeName + ' --amend-draft 留痕重锚后重新 --done。')
1417
- writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'draft 篡改门禁阻断' })
1418
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1419
- }
1420
- } catch { /* 门禁异常 fail-soft(不是篡改,对齐探针 degraded 放行) */ }
1421
- // ── ceremony 双跑收口·第一出口(2026-09-18-ceremony-risk-pricing task-04 / FR-03 / D-003;
1422
- // execute 评审 FAIL 项补接线):声明档(.runtime/ceremony-tier-<change>.json 开跑定价)vs
1423
- // 实际 diff 重跑 blast+span 的事实档,mismatch(severity=error,懒 agent 低报)汇入 verify
1424
- // errors 硬 flag 面——阻断回滚(照 reconcile/probe 先例 rollbackCompletionAndReturn,修复后
1425
- // 重跑 --done 进度不丢)。检查/记账/种子收敛在 runCeremonyDualRunCheck 单点(archive 出口
1426
- // complete-handlers.js runArchiveCeremonyDualRunExit 同款消费);skipped(档位文件缺失=存量
1427
- // 变更零红门禁)/degraded(git 不可用降级)零噪音放行;整体 fail-soft——检查自身异常降级
1428
- // 放行(异常不是低报,对齐 archive 出口同款处置)。specBase/runtimeRoot 与四邻检查同源
1429
- // (reconcileRuntimeRoot=resolveRuntimeRoot(platformOpts, specBase),口径一处定义不二算)。──
1430
- try {
1431
- const { runCeremonyDualRunCheck, printCeremonyDualRunCheck } = await import('../verify-postcheck.js')
1432
- const ceremonyCheck = await runCeremonyDualRunCheck({
1433
- cwd, specBase, changeName,
1434
- runtimeRoot: reconcileRuntimeRoot,
1435
- })
1436
- printCeremonyDualRunCheck(ceremonyCheck)
1437
- if (ceremonyCheck.status === 'mismatch' && ceremonyCheck.severity === 'error') {
1438
- return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
1439
- }
1440
- } catch (e) {
1441
- console.warn(`⚠️ Ceremony 双跑对账异常降级放行(不阻断 verify,fail-soft): ${(e && e.message) || e}`)
1442
- }
1443
1440
  // ── 行号漂移自动重锚(2026-09-09 §7-6:quick --done 已接,verify/archive 收尾补接)──
1444
1441
  // 本变更 diff 中的 .md 文档经 autoReanchorDocRefs 定点重锚(fixable 唯一/优选命中才改 +
1445
1442
  // 同口径回执),消灭「src 改动平移行号 → 下次 docs gate 拦」的人工往返。挂 verify 收尾即
@@ -0,0 +1,37 @@
1
+ /**
2
+ * lint 归属鉴定降档(共享 helper)——2026-10-15-lint-triage-path-unify。
3
+ *
4
+ * 背景(消费仓坑档案 docs/sillyspec/verify-lint-triage-path-divergence.md,2026-10-15
5
+ * -dsh-engine-acp verify 实证):降档判定原先只实现在 gates.js 的 verify --done 拦截面;
6
+ * verify step6「noAI 质量扫描」(verify-quality-scan.js)只做 shouldBlockVerifyLint 二值
7
+ * 判定直接 throw——同一失败两路径行为分叉(step6 throw / --done 降档)。本模块把判定与
8
+ * 提示文案收敛为单一来源,两路径同接线。
9
+ *
10
+ * 语义(R4-S-F 实证口径,与 gates.js 原内联实现等价):
11
+ * - pre-existing(失败提及文件 × 本变更文件集零交集)→ 打印降档 warning,返回 true(放行);
12
+ * - owned(有交集)/ unattributable(输出无可识别路径)/ 鉴定链异常 → 返回 false
13
+ * (维持硬拦,保守——无法排除本变更责任时宁可拦不可漏)。
14
+ *
15
+ * @param {{lintCheck?: {failureFiles?: string[]}, cwd: string, changeName: string, specBase: string, importer?: () => Promise<any>}} input
16
+ * importer 可注入(测试用);缺省动态加载 ../verify-postcheck.js(triageLintOwnership +
17
+ * resolveVerifyChangedFiles——运行时 import 避免模块加载环)。
18
+ * @returns {Promise<boolean>} true=降档放行(advisory),false=维持硬拦。
19
+ */
20
+ export async function lintOwnershipDowngrade({ lintCheck, cwd, changeName, specBase, importer }) {
21
+ const load = importer || (async () => import('../verify-postcheck.js'))
22
+ let lintOwnership = null
23
+ try {
24
+ const { triageLintOwnership, resolveVerifyChangedFiles } = await load()
25
+ const lintScopeFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
26
+ .map((f) => String(f).replace(/\\/g, '/')).filter((f) => !f.startsWith('.sillyspec/'))
27
+ lintOwnership = triageLintOwnership({ failureFiles: (lintCheck && lintCheck.failureFiles) || [], changeFiles: lintScopeFiles })
28
+ } catch { /* 鉴定链异常维持硬拦(保守) */ }
29
+ if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
30
+ const failureFiles = (lintCheck && lintCheck.failureFiles) || []
31
+ console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 推进:')
32
+ console.warn(` 存量债文件:${failureFiles.join('、') || '(见上方输出)'}`)
33
+ console.warn(' 处置建议:单独清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
34
+ return true
35
+ }
36
+ return false
37
+ }