sillyspec 3.31.0 → 3.32.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 (74) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
  5. package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
  6. package/.zcodeignore +66 -0
  7. package/AGENTS.md +16 -16
  8. package/assets/command-cards/flow.md +9 -3
  9. package/assets/command-cards/run-quick.md +4 -1
  10. package/package.json +2 -2
  11. package/src/agent-session-log.js +27 -5
  12. package/src/config-schema.js +21 -0
  13. package/src/datetime.js +51 -0
  14. package/src/db.js +10 -3
  15. package/src/decision-distill.js +21 -12
  16. package/src/design-facts.js +46 -6
  17. package/src/flow-draft.js +468 -95
  18. package/src/flow-parity.js +62 -4
  19. package/src/flow-review.js +136 -6
  20. package/src/flow.js +729 -125
  21. package/src/foreign-declared.js +71 -3
  22. package/src/fr-index.js +148 -162
  23. package/src/git-helper.js +16 -3
  24. package/src/hooks/worktree-guard.js +16 -5
  25. package/src/hunk-attribution.js +28 -5
  26. package/src/index.js +186 -19
  27. package/src/knowledge-classify.js +3 -1
  28. package/src/knowledge-digest.js +55 -26
  29. package/src/knowledge-match.js +135 -7
  30. package/src/knowledge-stats.js +66 -4
  31. package/src/knowledge-vector.js +160 -0
  32. package/src/machine-interface.js +90 -2
  33. package/src/module-impact.js +22 -1
  34. package/src/modules.js +9 -0
  35. package/src/progress/change-registry.js +20 -0
  36. package/src/progress/stage-machine.js +4 -7
  37. package/src/progress.js +8 -1
  38. package/src/quicklog.js +20 -0
  39. package/src/redomain.js +23 -7
  40. package/src/route-hindsight.js +276 -0
  41. package/src/run/archive-distill.js +3 -1
  42. package/src/run/command.js +65 -12
  43. package/src/run/complete-handlers.js +219 -5
  44. package/src/run/complete.js +77 -11
  45. package/src/run/gate-snapshot.js +15 -0
  46. package/src/run/gates.js +60 -4
  47. package/src/run/prompt.js +159 -31
  48. package/src/run/quick-audit.js +66 -7
  49. package/src/run/shared.js +6 -11
  50. package/src/run/stage.js +5 -1
  51. package/src/run/verify-quality-scan.js +84 -0
  52. package/src/scan-facts.js +2 -1
  53. package/src/scope-audit.js +9 -2
  54. package/src/sentinel-assertions.js +67 -16
  55. package/src/stage-contract-engine.js +4 -1
  56. package/src/stage-contract-spec.js +103 -4
  57. package/src/stage-contract.js +55 -7
  58. package/src/stages/brainstorm.js +27 -14
  59. package/src/stages/knowledge.js +4 -3
  60. package/src/stages/plan-postcheck.js +98 -3
  61. package/src/stages/plan.js +22 -2
  62. package/src/stages/quick.js +1 -1
  63. package/src/stages/verify.js +2 -1
  64. package/src/task-review.js +1 -1
  65. package/src/task-tick.js +121 -0
  66. package/src/taskcard-frontmatter.js +31 -0
  67. package/src/ui-visual.js +43 -8
  68. package/src/verify-postcheck.js +509 -37
  69. package/src/verify-probes.js +216 -5
  70. package/src/watcher.js +1067 -1031
  71. package/src/worktree-apply.js +9 -1
  72. package/src/worktree.js +21 -6
  73. package/templates/agents-instruction.md +15 -15
  74. package/templates/prompts/taskcard-rules.md +1 -0
package/src/flow.js CHANGED
@@ -25,13 +25,13 @@
25
25
  * 六子步完成标记/legacy_fallback/route_hint)——fs-atomic 原子写,缺文件=未参与 thin。
26
26
  * 幂等循 task-done 先例:子步各查自身完成标记,中断半态重入断点续,中段失败精确报告。
27
27
  */
28
- import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync } from 'node:fs'
28
+ import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
29
29
  import { createHash } from 'node:crypto'
30
- import { join, relative } from 'node:path'
30
+ import { join, relative, dirname } from 'node:path'
31
31
  import yaml from 'js-yaml'
32
32
  import { git, gitQuiet } from './git-helper.js'
33
33
  import { writeAtomicSync } from './fs-atomic.js'
34
- import { resolveRuntimeRoot, triggerSync } from './run/shared.js'
34
+ import { resolveRuntimeRoot, triggerSync, assertDatedChangeName } from './run/shared.js'
35
35
  import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
36
36
  import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
37
37
 
@@ -108,6 +108,62 @@ function changedFilesSinceBaseline(cwd, baselineCommit) {
108
108
  return [...files]
109
109
  }
110
110
 
111
+ /** 递归取路径最新 mtime(目录取全体成员最大值——目录自身 mtime 只随成员增删动,不随成员内容写动)。
112
+ * stat/readdir 失败与超帽一律返回 Infinity(=「新」,调用方保守保留——评审 P3 清偿:readdir
113
+ * 抛错曾返回 stat 时刻的目录自身 mtime,窄角竞态下会把真被干活触及的目录误剔)。安全帽:walk
114
+ * 预算 2000 条(巨型未跟踪树防遍历失控)。开放世界:时间戳是唯一裁判,不按路径形态设例外。
115
+ * statFn 注入缝(resolveArchiveCommitPathspecs 的 existsFn 同款先例):仅测试可达。 */
116
+ function newestMtime(p, budget = { left: 2000 }, statFn = statSync) {
117
+ let st
118
+ try { st = statFn(p) } catch { return Infinity }
119
+ if (!st.isDirectory()) return st.mtimeMs
120
+ let max = st.mtimeMs
121
+ let entries
122
+ try { entries = readdirSync(p, { withFileTypes: true }) } catch { return Infinity }
123
+ for (const e of entries) {
124
+ if (--budget.left < 0) return Infinity
125
+ const m = newestMtime(join(p, e.name), budget, statFn)
126
+ if (m > max) max = m
127
+ }
128
+ return max
129
+ }
130
+
131
+ /**
132
+ * 重入路由面的未跟踪前置过滤(坑 resume-domain-flip,2026-10-06 轻量道实测):resume 域路由
133
+ * 复用 changedFilesSinceBaseline(fr-rot-precision ⑥ 口径——含未跟踪是对的:干活期创建未提交
134
+ * 的交付文件必须路由),但 `git status --porcelain` 会把**先于变更存在**的他侧未跟踪遗留一并
135
+ * 扫进来;重入早于干活时区间 diff 为空、本变更无工作树改动,路由面只剩这批垃圾 → 域被劫持成
136
+ * 伪域、fresh 简报的 input 域与 FR 注入全丢。判据是时间不是路径形态:未跟踪(??)条目的最新
137
+ * mtime 早于变更出生时刻 = 本变更存活期内从未写过 = 不属本变更的路由面,剔除。
138
+ * 保守保留(行为同现状):birthTs 空/非有限、stat 失败、walk 超帽。跟踪条目(区间 diff、M/D)
139
+ * 不过此滤——提交与工作树修改是明确的变更事实。
140
+ * @param {string} cwd 工作区根
141
+ * @param {string[]} files changedFilesSinceBaseline 产出的路径面(含 `??` 原样目录条目)
142
+ * @param {number|null} birthTs 变更出生时刻(epoch 毫秒;null=不过滤)
143
+ * @param {{ statFn?: (p: string) => object }} [opts] 测试注入缝(statFn 缺省真 statSync)
144
+ * @returns {string[]} 过滤后的路径面
145
+ */
146
+ export function filterPreChangeUntracked(cwd, files, birthTs, opts = {}) {
147
+ if (!Array.isArray(files) || files.length === 0) return Array.isArray(files) ? files : []
148
+ if (typeof birthTs !== 'number' || !Number.isFinite(birthTs)) return files
149
+ const s = gitQuiet(cwd, ['status', '--porcelain'])
150
+ if (!s) return files
151
+ const untracked = new Set()
152
+ for (const line of String(s).split('\n')) {
153
+ if (!line || line.length < 4) continue
154
+ if (!line.startsWith('??')) continue
155
+ const p = line.slice(3).trim().replace(/^"|"$/g, '')
156
+ if (p) untracked.add(p.replace(/\\/g, '/'))
157
+ }
158
+ if (untracked.size === 0) return files
159
+ const statFn = typeof opts.statFn === 'function' ? opts.statFn : statSync
160
+ return files.filter((f) => {
161
+ const norm = String(f).replace(/\\/g, '/')
162
+ if (!untracked.has(norm)) return true // 非 ?? 条目(跟踪/区间 diff)不滤
163
+ return !(newestMtime(join(cwd, norm), { left: 2000 }, statFn) < birthTs) // 最新 mtime 早于出生时刻 → 剔除
164
+ })
165
+ }
166
+
111
167
  /** 材料路径清单(稳定前缀——缓存最优;按在场性列出,不读内容)。 */
112
168
  function materialPaths(specBase, changeName, changeDir) {
113
169
  const paths = [
@@ -131,6 +187,34 @@ function extractInputPaths(input) {
131
187
  return [...out]
132
188
  }
133
189
 
190
+ /**
191
+ * 域路由用 input 路径样 token:在场或图内过滤(坑 module-map-list-leak 实测次生——散文斜杠词
192
+ * git/DB/JSON 被当路径参与路由,冒充真域 server-parser)。判据:token 相对 cwd 在文件系统在场
193
+ * (existsSync),或被模块图声明的路径覆盖(matchedModuleIds——图内尚不存在的目标文件照常路由,
194
+ * thin-fr-inject-parity ④ 契约:--input 提到将新建的 src/cli/login.js 须命中 cli 域)。
195
+ * 两判据都不建前缀/扩展名白名单——文件系统与仓内模块图即开放世界裁判;散文斜杠词两者皆不
196
+ * 成立即出局(走「起点无依据」诚实提示)。绿地草案(bsPaths)不过滤:绿地语料允许指向
197
+ * 尚不存在目标,模块图从零起草恰需它们。
198
+ * @param {string} cwd 工作区根
199
+ * @param {string} specBase .sillyspec 根
200
+ * @param {string} input --input 语料
201
+ * @returns {string[]} 在场或图内的路径样 token
202
+ */
203
+ export async function extractRoutingInputPaths(cwd, specBase, input) {
204
+ let moduleIndex = null
205
+ let matchedModuleIds = null
206
+ try {
207
+ const dd = await import('./decision-distill.js')
208
+ const fi = await import('./fr-index.js')
209
+ moduleIndex = dd.discoverModuleIndex(join(specBase, 'knowledge'))
210
+ matchedModuleIds = fi.matchedModuleIds
211
+ } catch { /* best-effort:图缺席则只按在场判 */ }
212
+ return extractInputPaths(input).filter((p) => {
213
+ try { if (existsSync(join(cwd, p))) return true } catch { /* 落到图内判 */ }
214
+ try { return (matchedModuleIds(moduleIndex, p) || []).length > 0 } catch { return false }
215
+ })
216
+ }
217
+
134
218
  /**
135
219
  * 轻量道知识注入段(2026-09-25-thin-fr-inject-parity):默认快道读取面对齐——
136
220
  * {FR_INDEX_DIGEST}/{DECISION_HITS}/execute 知识命中报告此前只在 run 族装配,轻量道 fresh
@@ -146,18 +230,35 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
146
230
  try {
147
231
  const knowledgeRoot = join(specBase, 'knowledge')
148
232
  const { discoverModuleIndex } = await import('./decision-distill.js')
149
- const { resolveTouchedDomains, readActiveFrDigest } = await import('./fr-index.js')
233
+ const { resolveTouchedDomains, readActiveFrDigest, rankFrDigestForInjection, deliverableFilesFromDesignText } = await import('./fr-index.js')
150
234
  const moduleIndex = discoverModuleIndex(knowledgeRoot)
151
235
  let domains = []
152
236
  let basis = ''
237
+ let touched = []
153
238
  if (Array.isArray(filesOverride) && filesOverride.length > 0) {
154
- domains = resolveTouchedDomains(changeDir, moduleIndex, filesOverride).filter((d) => d !== 'unmapped')
239
+ domains = resolveTouchedDomains(changeDir, moduleIndex, filesOverride, knowledgeRoot).filter((d) => d !== 'unmapped')
155
240
  basis = filesOverride.length > 0 ? 'input/diff 路径' : ''
241
+ touched = filesOverride
156
242
  } else if (existsSync(join(changeDir, 'design.md'))) {
157
- domains = resolveTouchedDomains(changeDir, moduleIndex).filter((d) => d !== 'unmapped')
243
+ domains = resolveTouchedDomains(changeDir, moduleIndex, null, knowledgeRoot).filter((d) => d !== 'unmapped')
158
244
  basis = 'design.md 交付清单'
245
+ try {
246
+ touched = deliverableFilesFromDesignText(readFileSync(join(changeDir, 'design.md'), 'utf8'))
247
+ } catch { /* design 不可读=无触碰面 */ }
159
248
  }
160
249
  const frs = domains.length > 0 ? readActiveFrDigest(knowledgeRoot, domains) : []
250
+ // 注入排序(2026-10-03-fr-inject-relevance-rank):TierA 覆盖命中(🎯)置前、TierB 日期新→旧
251
+ // ——取代文件序前 8(域增长后注入面恒为最老 8 条,新立规格永不可见)。触碰面与域路由同源;
252
+ // fail-soft:两面皆缺=纯 TierB(保持 readActiveFrDigest 文件序)。
253
+ let tierAIds = new Set()
254
+ let frRanked = frs
255
+ if (frs.length > 0 && touched.length > 0) {
256
+ ({ ranked: frRanked, tierAIds } = rankFrDigestForInjection({ archiveRoot: join(specBase, 'changes', 'archive'), frs, changed: touched }))
257
+ }
258
+ // 骨架门(2026-10-03-fr-skeleton-gate):纯骨架条目不占注入席位(TierA 命中例外——它可能是
259
+ // 该文件唯一行为痕迹);指针行披露骨架数。查重/rot/绑定面不滤。
260
+ const injectable = frRanked.filter((f) => !f.skeleton || tierAIds.has(f.id))
261
+ const skeletonHidden = frs.filter((f) => f.skeleton && !tierAIds.has(f.id)).length
161
262
  summary.domains = domains
162
263
  summary.frCount = frs.length
163
264
  lines.push(`🧠 知识注入(轻量道读取面——现行 FR/否决决策/已知坑,动手前扫一眼):`)
@@ -169,30 +270,35 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
169
270
  if (frs.length === 0) {
170
271
  lines.push(` 现行 FR:(该域暂无 active FR 索引条目——本变更大概率是首批需求)`)
171
272
  } else {
172
- const flagged = frs.filter((f) => f.needsReview)
173
- const ordered = [...flagged, ...frs.filter((f) => !f.needsReview)]
174
- for (const f of ordered.slice(0, 8)) {
175
- lines.push(` - ${f.id} ${f.title}${f.needsReview ? ` ⚠️待复核(${f.needsReview})` : ''}${f.unconfirmed > 0 ? ` ⚪${f.unconfirmed}未确认绑定` : ''}`)
273
+ const renderedFr = injectable.slice(0, 8)
274
+ for (const f of renderedFr) {
275
+ lines.push(` - ${f.id} ${f.title}${tierAIds.has(f.id) ? ' 🎯' : ''}${f.unconfirmed > 0 ? ` ⚪${f.unconfirmed}未确认绑定` : ''}`)
176
276
  }
177
- if (ordered.length > 8) lines.push(` (+${ordered.length - 8} 条见 knowledge/fr/ 对应域文件)`)
277
+ const hiddenCount = frs.length - renderedFr.length
278
+ if (hiddenCount > 0) lines.push(` (+${hiddenCount} 条见 knowledge/fr/ 对应域文件${skeletonHidden > 0 ? `;纯骨架 ${skeletonHidden} 条不注入` : ''}${tierAIds.size > 0 ? ';🎯=与本次触碰文件有覆盖交集' : ''})`)
178
279
  // 抽查确认(2026-09-27-confirm-on-use 三层治理①层):干活中本来就在消费这些条目——
179
280
  // 相符则收口前翻牌(机械防橡皮图章:--evidence 必须是可解析的真实测试路径),
180
281
  // 不符留给 knowledge digest 信号。至多点名 2 个(抽查式,防全勾仪式化)。
181
- const unconfirmed = ordered.filter((f) => f.unconfirmed > 0).slice(0, 2)
282
+ const unconfirmed = injectable.filter((f) => f.unconfirmed > 0).slice(0, 2)
182
283
  if (unconfirmed.length > 0) {
183
- lines.push(` 🔍 抽查确认(至多 ${unconfirmed.length} 条,干活中顺带核):${unconfirmed.map((f) => f.id).join('、')} —— 绑定与实态相符则收口前 \`sillyspec tests confirm --anchor <id> --evidence <真实测试路径>\`(翻 active);不符则不动,留给 knowledge digest 信号`)
284
+ lines.push(` 🔍 抽查确认(至多 ${unconfirmed.length} 条,干活中顺带核):${unconfirmed.map((f) => f.id).join('、')} —— 绑定与实态相符则收口前 \`sillyspec tests --confirm --anchor <id> --evidence <真实测试路径>\`(翻 active;--confirm 是 flag 非子命令——形态与 index.js 实现及其用法文案一致,2026-10-05-tests-confirm-hint 修正错形态静默空转);不符则不动,留给 knowledge digest 信号`)
184
285
  }
185
286
  }
186
287
  }
187
- const { matchKnowledge } = await import('./knowledge-match.js')
188
- const km = matchKnowledge(knowledgeRoot, `${change}\n${input || ''}`)
189
- const rejected = (km.decisionHits || []).filter((h) => h.status === 'rejected')
288
+ const { matchKnowledgeHybrid } = await import('./knowledge-vector.js')
289
+ const { deathPathNote } = await import('./knowledge-match.js')
290
+ const km = await matchKnowledgeHybrid(knowledgeRoot, `${change}\n${input || ''}`, { cwd: dirname(specBase) })
291
+ // 防复潮面 = 死路注记 ∪ 有主题重叠的 rejected(2026-09-28-knowledge-inject-ranking 起 rejected∪死路;
292
+ // 2026-09-28-knowledge-gate-denoise 收紧:score 零的 rejected 与查询零主题重叠——空标题条目靠状态
293
+ // 蹭进回显是行为实测三例的噪音源,不再注入)
294
+ const rejected = (km.decisionHits || []).filter((h) => h.deathPath || (h.status === 'rejected' && h.score > 0))
190
295
  summary.rejectedDecisions = rejected.length
191
296
  summary.knowledgeEntries = km.matched ? (km.entries || []).length : 0
192
297
  if (rejected.length > 0) {
193
- lines.push(` ⚠️ 否决决策(历史已否决,防复潮——除非复潮条件满足勿重提,复潮须在本变更 decisions.md 记新版本):`)
298
+ lines.push(` ⚠️ 否决决策/死路注记(历史已否决或已记死路,防复潮——除非复潮条件满足勿重提,复潮须在本变更 decisions.md 记新版本):`)
194
299
  for (const h of rejected.slice(0, 5)) {
195
- lines.push(` - ${h.id} ${h.title}(${h.file})否决理由:${h.reason || '(未记录)'}`)
300
+ const note = h.status === 'rejected' ? (h.reason || '(未记录)') : `死路——${deathPathNote(h.reason)}`
301
+ lines.push(` - ${h.id} ${h.title}(${h.file})${h.status === 'rejected' ? '否决理由' : '⚰️'}:${note}`)
196
302
  }
197
303
  }
198
304
  if (km.matched && (km.entries || []).length > 0) {
@@ -215,22 +321,22 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
215
321
  * quick-done 钩子迁轻量道 + 按文件面交集判相关度(域级全标→三分判据,评审实测基线 131 条 →
216
322
  * strong 83 / unknown 8 / skip 40)。对触达域每条 active FR:coverage = frCoverageFiles(来源变更)
217
323
  * ∪ bindings,与本次归属文件面单向匹配(changed 恒文件级:相等 || changed.startsWith(cov 补/):
218
- * 交集非空 → strong(打待复核标记,下次知识注入带 ⚠️);coverage 空 → unknown(不打标,遥测
219
- * 单列——宁漏勿滥:漏标只损失注入排序优先级);非空无交集 → skip。
324
+ * 交集非空 → strong(只进收口 advisory 与遥测——2026-09-29-rot-retire-inject-cap 起不再落盘
325
+ * 待复核标记:371 条零消费实证,持久化已拆);coverage 空 → unknown(遥测
326
+ * 单列——宁漏勿滥);非空无交集 → skip。
220
327
  * 遥测 count 语义=strong(防污染 knowledge-stats 的 rotSuspectByDomain 消费读数,评审 P1-1)。
221
328
  * advisory 零阻断,fail-open。
222
- * @returns {{ warn: string|null, warnInfo?: string|null, domains: string[], strong: number, unknown: number, skip: number, marked: number }}
329
+ * @returns={{ warn: string|null, warnInfo?: string|null, domains: string[], strong: number, unknown: number, skip: number }}
223
330
  */
224
331
  export async function rotSuspectFlow({ specBase, change, changeDir, files }) {
225
332
  // 查询核迁 fr-index.activeFrCoverageHits 单源(2026-09-26-dynamic-test-inference)——
226
- // 同一「覆盖面∩触碰文件」查询双消费:本函数打待复核标记(信号面);verify 门
333
+ // 同一「覆盖面∩触碰文件」查询双消费:本函数出收口 advisory 与遥测(信号面);verify 门
227
334
  // collectFrLinkedTests 反用为需求关联回归测试面。判据语义逐字不变。
228
- const { activeFrCoverageHits, markFrNeedsReview } = await import('./fr-index.js')
335
+ const { activeFrCoverageHits } = await import('./fr-index.js')
229
336
  const q = activeFrCoverageHits({ specBase, change, changeDir, files })
230
- const knowledgeRoot = join(specBase, 'knowledge')
231
337
  const { domains, hits: strong, unknownSources, unknownFrCount, skip } = q
232
338
  if (domains.length === 0) {
233
- return { warn: null, warnInfo: null, domains, strong: 0, unknown: 0, skip: 0, marked: 0 }
339
+ return { warn: null, warnInfo: null, domains, strong: 0, unknown: 0, skip: 0 }
234
340
  }
235
341
  const { appendKnowledgeHit } = await import('./knowledge-hits.js')
236
342
  appendKnowledgeHit(join(specBase, '.runtime'), {
@@ -239,19 +345,17 @@ export async function rotSuspectFlow({ specBase, change, changeDir, files }) {
239
345
  // 非来源变更数);unknownSources 披露来源名单(FR 多条可共享一个来源变更)
240
346
  strong: strong.length, unknown: unknownFrCount, skip, count: strong.length, // count=strong:knowledge-stats 消费口径(评审 P1-1)
241
347
  unknownSources: [...unknownSources], source: 'flow-done',
348
+ // frIds(2026-10-03-fr-governance-telemetry):strong 命中条目 id 帽 20——L3 裁决从域级计数
349
+ // 升为条目级有的放矢(stats 裁决候选视图的聚合键)。
350
+ frIds: strong.slice(0, 20).map((h) => h.id),
242
351
  })
243
- let marked = 0
244
- if (strong.length > 0) {
245
- const mr = markFrNeedsReview(knowledgeRoot, strong.map((f) => f.id), change)
246
- marked = mr.marked
247
- }
248
352
  const warn = strong.length > 0
249
- ? `⚠️ [FR 腐烂 suspect·advisory] 触达 ${domains.join('、')} 域的 ${strong.length} 条 active FR 与本次交付文件面有覆盖交集——若改动影响这些行为,请在 requirements 承接/supersede 对账(已打待复核标记 ${marked} 条;下次知识注入带 ⚠️)`
353
+ ? `⚠️ [FR 腐烂 suspect·advisory] 触达 ${domains.join('、')} 域的 ${strong.length} 条 active FR 与本次交付文件面有覆盖交集——若改动影响这些行为,请在 requirements 承接/supersede 对账(收口提示即止,不留账)`
250
354
  : null
251
355
  const warnInfo = unknownFrCount > 0
252
- ? `ℹ️ 另有 ${unknownFrCount} 条 active FR 无法判定覆盖面(来源变更无归档件且无测试绑定,不计入 suspect,不打标):${unknownSources.slice(0, 5).join('、')}${unknownSources.length > 5 ? ' 等' : ''}`
356
+ ? `ℹ️ 另有 ${unknownFrCount} 条 active FR 无法判定覆盖面(来源变更无归档件且无测试绑定,不计入 suspect):${unknownSources.slice(0, 5).join('、')}${unknownSources.length > 5 ? ' 等' : ''}`
253
357
  : null
254
- return { warn, warnInfo, domains, strong: strong.length, unknown: unknownFrCount, skip, marked }
358
+ return { warn, warnInfo, domains, strong: strong.length, unknown: unknownFrCount, skip }
255
359
  }
256
360
 
257
361
  /**
@@ -318,7 +422,7 @@ function changeArtifactPaths(changeDir) {
318
422
  * flow start —— 第 1 次协议调用(建卡+下发)。已存在 change → 恢复简报(不新建不重置)。
319
423
  * @param {{change:string, input?:string, thick?:boolean, withTasks?:boolean, cwd:string, specBase:string, json?:boolean}} p
320
424
  */
321
- export async function cmdFlowStart({ change, input, thick = false, withTasks = false, reviewForce = null, cwd, specBase, runtimeRootOpt = null, json = false }) {
425
+ export async function cmdFlowStart({ change, input, title: titleFlag = null, thick = false, withTasks = false, reviewForce = null, autopilot = false, cwd, specBase, runtimeRootOpt = null, json = false }) {
322
426
  // local.yaml 缺席 fail-fast(R22 实证:缺 local.yaml 时 flow done 测试门静默兜底裸
323
427
  // python -m pytest → aiobotocore 假红/全量 860s 撞 600s 帽 → 11 轮重试 135min)。
324
428
  // local.yaml 是 init 的职责面——flow start 在此拒绝,不让 agent 在错误的测试配置上走完全程
@@ -363,11 +467,30 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
363
467
  const b = ensureBindingSlots({ changeDir })
364
468
  if (b.appended) console.log(`📌 收编追加测试绑定槽 ${b.slots} 枚(brainstorm requirements 无绑定面——干活时作答,flow done 校验)`)
365
469
  } catch (e) { console.warn(`⚠️ 收编补件失败(best-effort): ${(e && e.message) || e}`) }
470
+ // route-hindsight 首版快照(收编时点=redraftMissingArtifacts 首次落盘后):adopt 的
471
+ // design 是头脑风暴产物、机器只补缺件——快照取收编时点内容,此后 agent 改写计入指标。
472
+ try {
473
+ const { snapshotBaseline, baselineSha256 } = await import('./route-hindsight.js')
474
+ snapshotBaseline({ specBase, change, changeDir })
475
+ const _bsha = baselineSha256({ specBase, change })
476
+ if (_bsha && !(readFlowState(changeDir) || {}).baseline_sha256) writeFlowState(changeDir, { baseline_sha256: _bsha }) // 锚定首写者胜——重入不刷新
477
+ } catch { /* 快照 best-effort(缺失=指标零信号) */ }
366
478
  try {
367
479
  const { spawnWatcher } = await import('./watcher.js')
368
480
  const w = await spawnWatcher(cwd, change, { specBase })
369
481
  if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
370
482
  } catch { /* 观测旁路 best-effort */ }
483
+ // adopt 标题(用户需求 2026-09-29):头脑风暴 proposal 的 H1 是人写语义标题,
484
+ // deriveTitleFromLinkedChange 提取;--title 优先;兜底变更名。adopt 路径此前完全不
485
+ // 注册 changes 行——initChange 补插(幂等),既有行 title 空时回填。
486
+ try {
487
+ const { deriveTitleFromLinkedChange, deriveChangeTitle } = await import('./quicklog.js')
488
+ const adoptTitle = titleFlag || deriveTitleFromLinkedChange(specBase, change) || deriveChangeTitle(input) || change
489
+ pm.initChange(cwd, change, { title: adoptTitle })
490
+ const _arow = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
491
+ if (_arow && !_arow.title) pm.updateChangeMeta(cwd, change, { title: adoptTitle })
492
+ console.log(`🏷️ 变更标题:${adoptTitle}(收编自头脑风暴产物;重入 --title 可改)`)
493
+ } catch { /* adopt 标题 best-effort */ }
371
494
  const materials = materialPaths(specBase, change, changeDir)
372
495
  const artifacts = changeArtifactPaths(changeDir)
373
496
  const prototypePaths = artifacts.filter((p) => /\.html$/i.test(p))
@@ -382,10 +505,12 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
382
505
  prototypePaths.length > 0
383
506
  ? `🖼️ 原型在场(${prototypePaths.length} 个 HTML):实现前必看,界面/交互/流程按原型对齐;有出入以 design.md 承诺为准并在回复中说明。`
384
507
  : null,
385
- `【你要做的】直接干活:改代码、写测试。design/decisions 是本变更的承诺锚(flow done 豁免 design 四节槽,以其为准)。`,
508
+ `【你要做的】① spec 阶段先定稿任务面:把 tasks.md 改写为工作分解(每行=可独立完成并当场验证的一步:做什么+怎么验证;全 \`- [ ]\`——代码开动前工作队列先存在);`,
509
+ `② 执行走任务循环:对 tasks.md 每个 pending 行——展示「Working on task N/M: <任务>」→ 做 → 测试绿后当场勾一格(\`- [ ]\`→\`- [x]\`:sillyspec task tick --change ${change} --task task-NN 或直接 Edit)→ 下一行;全勾后 flow done(收口硬门拒单拍多格勾选)。design/decisions 是本变更的承诺锚(flow done 豁免 design 四节槽,以其为准)。`,
386
510
  `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
387
- `✅ 任务面归你(thin-agent-tasks):tasks.md 是机器预填的标准逐条草稿——按实际实现路径覆写它(增删改组随意,保持 \`- [ ] task-NN:\` 行形态),完成一个你自己的任务单元即勾 \`- [x]\`——`,
388
- ` 勾选是收口哨兵的证据面:全勾但零提交 token/review.json 会被拒收;flow status 随时看勾选进度。`,
511
+ `✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
512
+ ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;`,
513
+ ` flow status --change ${change} 为自愿查看/恢复面(非协议必需,D-007 中间零必需交互;恢复时给下一任务指针与进度)。`,
389
514
  `⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
390
515
  ``,
391
516
  `【协议调用 2/2(干完后)】sillyspec flow done --change ${change}`,
@@ -420,6 +545,14 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
420
545
  console.log(`📌 重入补生成缺失机器稿 ${r.drafted.length} 件:${r.drafted.join('、')}(工具升级晚于 start 的在途变更补件;已存在文件未动)`)
421
546
  }
422
547
  } catch (e) { console.warn(`⚠️ 补起草失败(不阻断恢复简报): ${(e && e.message) || e}`) }
548
+ // route-hindsight 首版快照(resume 补件后,首写者胜):旧版本起步的在途变更此处首次取到
549
+ // 快照(偏晚=少计早期改写,方向保守不误标);恢复简报渲染零变化(快照静默落盘)。
550
+ try {
551
+ const { snapshotBaseline, baselineSha256 } = await import('./route-hindsight.js')
552
+ snapshotBaseline({ specBase, change, changeDir })
553
+ const _bsha2 = baselineSha256({ specBase, change })
554
+ if (_bsha2 && !(readFlowState(changeDir) || {}).baseline_sha256) writeFlowState(changeDir, { baseline_sha256: _bsha2 }) // 锚定首写者胜——resume 重入不刷新(审查 P1:重锚洗白篡改)
555
+ } catch { /* 快照 best-effort */ }
423
556
  // 声明通道 resume 生效(fr-governance-sweep 评审 P3 清偿):--review/--no-review 对在途
424
557
  // 变更重入 start 时落盘 review_force——与新变更/adopt 两路口径一致(此前 resume 只打简报
425
558
  // 即 return,flag 静默失效)。幂等:未带 flag(null)不覆盖既有声明。
@@ -427,14 +560,66 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
427
560
  writeFlowState(changeDir, { review_force: reviewForce })
428
561
  console.log(`⚖️ 已更新评审声明通道:review_force=${reviewForce}(resume 落盘)`)
429
562
  }
563
+ // autopilot resume 声明(2026-10-04-thin-docs-v2):重入带 --autopilot 补记豁免
564
+ if (autopilot && !(readFlowState(changeDir) || {}).autopilot) {
565
+ writeFlowState(changeDir, { autopilot: true })
566
+ console.log(`🤖 已声明 autopilot:spec 断点人审豁免(用户显式声明留痕)`)
567
+ }
568
+ // 变更标题补写(用户需求 2026-09-29:title=中文概括 ≤50 字,建议 ~20 字)——存量在途
569
+ // 变更 title 空/缺失时的回填通道:显式 --title 恒生效(重入改标题);否则从 --input 首
570
+ // 行推导、仅当库内 title 为空才写(不覆盖已有语义标题)。best-effort 不阻断恢复简报。
571
+ try {
572
+ const { deriveChangeTitle } = await import('./quicklog.js')
573
+ const _row = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
574
+ const _cur = _row ? (_row.title || '') : ''
575
+ const _derived = titleFlag || deriveChangeTitle(input)
576
+ if (_derived && (titleFlag || !_cur)) {
577
+ pm.updateChangeMeta(cwd, change, { title: _derived })
578
+ console.log(`🏷️ 变更标题${titleFlag ? '已更新' : '已补写'}:${_derived}(面板显示用;建议 ~20 字中文概括,重入 --title 可改)`)
579
+ }
580
+ } catch { /* 标题补写 best-effort */ }
430
581
  // 知识注入(2026-09-25-thin-fr-inject-parity):resume 路径域路由走基线以来文件面——
431
582
  // changedFilesSinceBaseline(fr-rot-precision 评审 P2:含未提交工作树/untracked、剔 .sillyspec,
432
583
  // 与收口口径同源;裸 git diff 双提交区间会漏干活期未提交文件);best-effort 不阻断恢复简报。
433
584
  let resumeDigest = { lines: [], summary: null }
434
585
  try {
435
- resumeDigest = await flowKnowledgeDigest({ specBase, change, changeDir, input: null, filesOverride: changedFilesSinceBaseline(cwd, st.baseline_commit) })
586
+ // 重入注入回填(2026-10-05-flow-tail-polish):input:null 会把注入降级为「语料未命中」,
587
+ // 首次 start 给的抽查确认指引断点恢复后丢失——flow-state.input 优先,proposal 动机
588
+ // 「任务原话转写:」剥前缀回退(存量 change 无 input 字段走此路;「(未提供 --input)」占位视为空)。
589
+ let resumeInput = typeof st.input === 'string' && st.input.trim() ? st.input : null
590
+ if (!resumeInput) {
591
+ try {
592
+ const pText = readFileSync(join(changeDir, 'proposal.md'), 'utf8')
593
+ const m = /任务原话转写:([^\n]+)/.exec(pText)
594
+ if (m && !m[1].includes('(未提供 --input)')) resumeInput = m[1].trim()
595
+ } catch { /* proposal 回退 best-effort */ }
596
+ }
597
+ // 路由面 = 过滤后的基线以来文件面 ∪ input 路径语料路由面(resume-domain-flip 修复:
598
+ // 未跟踪遗留先于变更存在 → 时间过滤剔除;并集保证重入知识面 ⊇ fresh——重入早于干活时
599
+ // 文件面只剩他侧垃圾,input 面是唯一真实依据)。出生时刻读进度库 changes.created_at
600
+ // (best-effort:无 DB/无行/解析失败不过滤,行为同现状)。
601
+ let birthTs = null
602
+ try {
603
+ const row = pm._ensureDB(cwd).getDb().prepare('SELECT created_at FROM changes WHERE name = ?').get(change)
604
+ if (row && row.created_at) {
605
+ const t = Date.parse(row.created_at)
606
+ if (Number.isFinite(t)) birthTs = t
607
+ }
608
+ } catch { /* 出生时刻 best-effort */ }
609
+ const diffFace = changedFilesSinceBaseline(cwd, st.baseline_commit)
610
+ const inputFace = resumeInput ? (await extractRoutingInputPaths(cwd, specBase, resumeInput)) : []
611
+ resumeDigest = await flowKnowledgeDigest({
612
+ specBase, change, changeDir, input: resumeInput,
613
+ filesOverride: [...new Set([...filterPreChangeUntracked(cwd, diffFace, birthTs), ...inputFace])],
614
+ })
436
615
  } catch { /* 注入 best-effort */ }
437
- printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot, st, digestLines: resumeDigest.lines })
616
+ // 恢复简报标题(2026-10-06-resume-title):getChangeTitle 单源只读(与 flow status 同族),
617
+ // best-effort——读取失败按无标题渲染(与现状输出一致)
618
+ let resumeTitle = null
619
+ try {
620
+ resumeTitle = new ProgressManager({ specDir: specBase }).getChangeTitle(cwd, change)
621
+ } catch { /* 标题 best-effort */ }
622
+ printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot, st, digestLines: resumeDigest.lines, title: resumeTitle })
438
623
  return { recovery: true }
439
624
  }
440
625
  }
@@ -448,8 +633,11 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
448
633
  console.error(`❓ 需求不够清晰(--input ${input ? '在场但「成功标准」条目提取 0 条' : '缺失'})——轻量跑道假定输入已含决策,两选一:`)
449
634
  console.error(` ① 头脑风暴预段(需求不明时推荐):sillyspec run brainstorm --change ${change}`)
450
635
  console.error(` 人机交互探索需求、出 design/决策/原型;完成后回来 sillyspec flow start --change ${change},产物自动收编续跑轻量变更`)
451
- console.error(` ② 确认输入已含决策:sillyspec flow start --change ${change} --input "<完整需求>",input 过门格式:`)
452
- console.error(` 先写动机/背景;随后独立一行只写「成功标准:」;再每行一条「- <可验证标准>」`)
636
+ console.error(` ② 确认输入已含决策:sillyspec flow start --change ${change} --input "<完整需求>"(引号内换行合法,可照抄形态):`)
637
+ console.error(' <动机/背景在前>')
638
+ console.error('')
639
+ console.error(' 成功标准:')
640
+ console.error(' - <可验证标准,一行一条>')
453
641
  process.exit(2)
454
642
  }
455
643
  }
@@ -459,7 +647,16 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
459
647
  // 形态信号:清晰度门管「需求说不清楚」(预段收编),升厚只留用户决策(--upgrade-thick 同意门)
460
648
  // 与运行时证据(实测失败升档/edit_ratio/评审——风险面在收口时点按承诺词/diff 原语/盲维判定)。
461
649
 
462
- pm.initChange(cwd, change, {})
650
+ // 变更标题(用户需求 2026-09-29:title=中文概括 ≤50 字、建议 ~20 字,agent 总结):
651
+ // --title 显式指定优先;否则 --input 首行推导(agent 在 input 首行写一句中文概括即为标题);
652
+ // 两者皆缺用变更名兜底(可辨识但不达意——横幅提示下次带 --title)。
653
+ let changeTitle = null
654
+ try {
655
+ const { deriveChangeTitle } = await import('./quicklog.js')
656
+ changeTitle = titleFlag || deriveChangeTitle(input) || change
657
+ } catch { changeTitle = titleFlag || change }
658
+ pm.initChange(cwd, change, { title: changeTitle })
659
+ console.log(`🏷️ 变更标题:${changeTitle}${changeTitle === change ? '(变更名兜底——下次带 --title "<≤20 字中文概括>" 更达意)' : '(面板显示用;建议 ~20 字中文概括,重入 flow start --title 可改)'}`)
463
660
  try {
464
661
  const { resolveSessionIdentity } = await import('./progress.js')
465
662
  const { session } = resolveSessionIdentity({ flagSession: null, cwd })
@@ -484,6 +681,12 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
484
681
  born_face: thick ? 'thick' : 'thin',
485
682
  with_tasks: Boolean(withTasks),
486
683
  review_force: reviewForce,
684
+ // input 原文留存(2026-10-05-flow-tail-polish):重入 start 知识注入回填的优先源
685
+ // (增量字段——读侧缺省容错,存量 change 走 proposal 转写回退)
686
+ input: typeof input === 'string' ? input : null,
687
+ // autopilot(2026-10-04-thin-docs-v2 FR-06):用户显式声明跳过 spec 断点人审(--autopilot);
688
+ // 缺省 false——flow done 要求 flow approve 批准证据(spec 断点机器化,护栏#2:零 prompt 劝说)
689
+ autopilot: Boolean(autopilot),
487
690
  baseline_commit: baseline,
488
691
  substeps: {},
489
692
  })
@@ -499,6 +702,24 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
499
702
  console.warn(`⚠️ 机器起草失败(轻量工件面降级为 flow done 默认验收;best-effort 不阻断): ${(e && e.message) || e}`)
500
703
  }
501
704
 
705
+ // route-hindsight 首版快照(FR-02 后门,2026-09-28-unclear-req-to-brainstorm):机器稿起草
706
+ // 时点锚定 design/tasks 首版副本(首写者胜幂等;R-04——取晚会让改写比恒 0 闭环失效)。
707
+ // 快照缺失=指标零信号(不误标),best-effort 不阻断 start。
708
+ try {
709
+ const { snapshotBaseline, baselineSha256 } = await import('./route-hindsight.js')
710
+ snapshotBaseline({ specBase, change, changeDir })
711
+ const _bsha3 = baselineSha256({ specBase, change })
712
+ if (_bsha3) writeFlowState(changeDir, { baseline_sha256: _bsha3 })
713
+ } catch { /* 快照 best-effort */ }
714
+
715
+ // route-hindsight 历史提示(FR-02 前门同屏注入):上个轻量变更被标记「疑似该走预段未走」
716
+ // 时点名(渲染在下方自检段上方)。无标记文件 → null 零注入(未升级/新装仓输出与现状一致)。
717
+ let hindsightHint = null
718
+ try {
719
+ const { readHindsightHint } = await import('./route-hindsight.js')
720
+ hindsightHint = readHindsightHint({ specBase })
721
+ } catch { /* 提示读取 best-effort 不阻断 start */ }
722
+
502
723
  const materials = materialPaths(specBase, change, changeDir)
503
724
  // 绿地 bootstrap(greenfield-bootstrap,R17 实证:无模块图仓 FR 全落伪域/unmapped,知识复利
504
725
  // 从第一条断流):模块图缺席且 --input 有路径语料 → 机器起草初始 _module-map.yaml 草案(按
@@ -521,21 +742,30 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
521
742
  } catch (e) { console.warn(`⚠️ 绿地模块图草案起草失败(best-effort 不阻断 start):${(e && e.message) || e}`) }
522
743
  // 知识注入(2026-09-25-thin-fr-inject-parity):fresh 起点域路由用 --input 提取的路径样
523
744
  // token(best-effort)——brainstorm 的 {FR_INDEX_DIGEST}/{DECISION_HITS} 注入面对齐到轻量道。
745
+ // 在场过滤(坑 module-map-list-leak):散文斜杠词(git/DB/JSON)不参与路由。
524
746
  let digest = { lines: [], summary: { domains: [], frCount: 0, rejectedDecisions: 0, knowledgeEntries: 0 } }
525
747
  try {
526
- digest = await flowKnowledgeDigest({ specBase, change, changeDir, input, filesOverride: extractInputPaths(input) })
748
+ digest = await flowKnowledgeDigest({ specBase, change, changeDir, input, filesOverride: await extractRoutingInputPaths(cwd, specBase, input) })
527
749
  } catch { /* 注入 best-effort 不阻断 start */ }
528
750
  const lines = [
529
751
  `🏃 flow start(${thick ? 'thick 厚档(--thick 显式声明,人声明不做启发式)' : 'thin 轻量跑道'}): ${change}`,
530
752
  `══════════════════════════════════════`,
753
+ ...(hindsightHint ? [hindsightHint, ''] : []),
754
+ `🔎 选道自检——对本需求,你还有没有必须问用户才能动手的问题?`,
755
+ ` 有 → sillyspec run brainstorm --change ${change}(它就是结构化问询协议,产物随后 flow start 自动收编续跑);`,
756
+ ` 无 → 继续轻量跑道。纯提示不阻断——不设声明 flag、不拦流程(选错了还有事后闭环指标兜底)。`,
757
+ ``,
531
758
  `【协议调用 1/2(本次)】change 已建 + 基线锚定(baseline_commit=${baseline ? baseline.slice(0, 10) : '(无 git 历史)'})${withTasks ? ' + 任务卡模式(--with-tasks:中间自愿用 task done,收尾仍 flow done)' : ''}`,
532
759
  ``,
533
760
  `【你要做的】直接干活:改代码、写测试。治理工件不用你写——flow done 机器做(协议记账单位=change 级)。`,
534
- `例外裁决面(唯一合法 .sillyspec 书写):AGENT 槽填充 / flow amend-draft(确要改机器稿时)。`,
535
- `⚠️ design.md 四节 AGENT 槽(做法/接口契约/边界并发四问/风险)动码前后顺手作答——每节至少一行,`,
536
- ` 写「不适用:<理由>」也算答;flow done 空槽拒收(承诺锚点,评审与 FR 对账都对着它)。`,
537
- `📜 requirements 的 FR 是机器摘录候选——语义要改写时直接编辑机器段后跑 flow amend-draft 留痕`,
538
- ` (槽里写的不进 FR 索引);输入含编号行为条目时机器已优先摘编号条目。`,
761
+ `书写面(纯 markdown 起草):requirements FR 正文/design 四节作答/测试绑定行都直接写正文——`,
762
+ ` 文档内零指纹标记、零槽注释、零书写指导(规则以本横幅与命令卡为唯一源);`,
763
+ ` 标题锚(FR 标题、四问文本)勿改写,收口做文档↔锚对比(锚失踪/空答拒收,成功标准漂移出 advisory)。`,
764
+ `⚠️ design.md 四节(做法/接口契约/边界并发四问/风险)动码前后顺手作答——答案写在问题下方,`,
765
+ ` 每节至少一行,写「不适用:<理由>」也算答;flow done 空节拒收(承诺锚点,评审与 FR 对账都对着它);`,
766
+ ` 列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)——勿写在「接口契约」节内(收口按节名解析声明面)。`,
767
+ `📜 requirements 每条 FR = 标题锚 + 一句带强度词的行为规定(必须/禁止/SHOULD/可以)+ 按需场景块`,
768
+ ` (#### 场景:名 + Given/When/Then);测试绑定节每条 FR 一行 \`FR-NN: test/路径「用例」\`。`,
539
769
  `📦 冻结面在 flow done 时点采集(baseline..HEAD 提交面):未提交交付文件——会话专属 worktree 自动并入;`,
540
770
  ` 共享主仓可带 --freeze-dirty 显式声明并入;git 中间提交归档后可 reset --soft 压扁为单提交`,
541
771
  ` (审计真相在 change.patch 冻结件 sha256 锚定,不依赖 git 历史形态)。`,
@@ -545,17 +775,23 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
545
775
  `⚠️ 交付纪律:收口前先把交付代码用显式 pathspec 提交(git add -- <文件> && git commit)——`,
546
776
  ` patch 冻结件范围=baseline..HEAD 提交面,未提交的代码不进审计件(R16 评审 P2 实证);`,
547
777
  ` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证)。`,
548
- `✅ 任务面归你(thin-agent-tasks):tasks.md 是机器预填的标准逐条草稿——按实际实现路径覆写它(增删改组随意,保持 \`- [ ] task-NN:\` 行形态),完成一个你自己的任务单元即勾 \`- [x]\`——`,
549
- ` 勾选是收口哨兵的证据面:全勾但区间提交的标题或正文均无 task-NN 且无 review.json 会被拒收;`,
550
- ` flow status 随时看勾选进度(②执行阶段勾选滞后时 status 会带提醒行——边干边勾)。`,
778
+ `✅ 任务面归你(工作分解契约,2026-10-07-thin-tasks-v3):tasks.md 是你的工作队列——机器种子按真实实现路径改写`,
779
+ ` (每行=可独立完成并当场验证的一步:做什么+怎么验证;增删改随意,保持 \`- [ ] task-NN:\` 行形态);`,
780
+ ` 执行循环:对每个未勾行 Working on task N/M → 做一件 → 测试绿后当场勾这一格 → 下一行——CLI 以 checkbox 为进度状态机`,
781
+ ` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
782
+ ` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
783
+ ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
784
+ ` flow status --change ${change} 为自愿查看/恢复面(恢复时给下一任务指针与进度)。`,
551
785
  ``,
552
786
  `🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
553
- ` ① spec 断点:填完 FR 区和 design 槽后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
554
- ` 等用户确认方案再动手写代码——方案错了返工最贵。`,
787
+ ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
788
+ ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
789
+ ` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2)。`,
555
790
  ` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
556
791
  ` 等用户确认再跑收口。`,
557
792
  ` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
558
- ` 用户明确说「直接跑完/不用问我」→ 三断点全跳过(agent 自主干到底)。`,
793
+ ` 用户明确说「直接跑完/不用问我」→ 重跑 flow start --change ${change} --autopilot 声明豁免(留痕),`,
794
+ ` ②③断点 agent 自主跳过。`,
559
795
  ` 随时可查进度:sillyspec flow status --change ${change}`,
560
796
  ...(detectUiTouch(input)
561
797
  ? [...buildUiGuidanceLines(), '']
@@ -580,8 +816,9 @@ export async function cmdFlowStart({ change, input, thick = false, withTasks = f
580
816
  return { change, baseline, materials }
581
817
  }
582
818
 
583
- /** 恢复简报:盘面状态(checkbox/提交/账本/dirty files)→ 做到哪、剩什么、下一步。 */
584
- function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot, st, digestLines = [] }) {
819
+ /** 恢复简报:盘面状态(checkbox/提交/账本/dirty files)→ 做到哪、剩什么、下一步。
820
+ * title(2026-10-06-resume-title):进度库登记的变更标题(string|null)——仅非空时在头两行后渲染。 */
821
+ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot, st, digestLines = [], title = null }) {
585
822
  const done = []
586
823
  const left = []
587
824
  for (const k of SUBSTEPS) (st.substeps?.[k] === 'done' ? done : left).push(k)
@@ -598,7 +835,8 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
598
835
  const hasLedger = existsSync(join(runtimeRoot, `verify-quality-scan-${change}.json`))
599
836
  console.log([
600
837
  `🔁 flow start 恢复简报(重入): ${change}(tier=${st.tier}${st.legacy_fallback ? ',legacy_fallback=true 混跑回退中' : ''})`,
601
- `══════════════════════════════════════`,
838
+ `══════════════════════════════════════════════════════`,
839
+ ...(title ? [`- 标题:${title}`] : []),
602
840
  `- 做到哪:任务勾选 ${checked}/${total};基线以来提交 ${commits} 个;变更/dirty 文件 ${dirty} 个;P2 质量扫描记录 ${hasLedger ? '在场' : '无'}。`,
603
841
  `- 六子步标记:${done.length ? `已完成 ${done.join('/')}` : '(无)'}${left.length ? `;待办 ${left.join('/')}` : ';全部完成'}`,
604
842
  `- 剩什么:${left.length === 0 && dirty === 0 ? '活已干完' : dirty > 0 ? '活未干完(继续改代码)' : '收尾待裁决'}`,
@@ -612,7 +850,20 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
612
850
  * 子步:artifacts(工件校验)→ ledger(账本对账+亲测)→ probes(探针)→ distill(决策提炼)
613
851
  * → archive(归档经 runArchiveChain,thin 轻量工件面跳过 plan.md 硬校验)→ events(事件收口)。
614
852
  */
615
- export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false }) {
853
+ /**
854
+ * 中断简报「待办」计算(2026-10-06-archive-cmd-race-and-brief:陈旧读修复)。
855
+ * 旧口径只看运行头快照 st.substeps——mark() 只写盘不回填内存快照,首轮运行时本轮刚
856
+ * 完成/跳过的子步全被误列进待办(实测 flow-status-json 首轮:已完成 artifacts、ledger、
857
+ * patch,待办仍列全部 8 子步)。正确口径 = 全部子步 − 盘上历史 done(快照)− 本轮
858
+ * doneList(含 (skip) 后缀)− 中断子步;重入时历史 done 已含上轮标记,两口径收敛同值。
859
+ * 纯函数,单测直接钉口径。
860
+ */
861
+ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [], failed } = {}) {
862
+ const doneNow = new Set((doneList || []).map((d) => String(d).replace(/\(skip\)$/, '')))
863
+ return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
864
+ }
865
+
866
+ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, allowBatchTick = false }) {
616
867
  const changeDir = join(specBase, 'changes', change)
617
868
  const st = readFlowState(changeDir)
618
869
  if (!st) {
@@ -659,36 +910,57 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
659
910
  } catch { /* 遥测 best-effort */ }
660
911
  }
661
912
  const reportMidFail = (failed) => {
662
- console.error(`❌ flow done 中断于子步「${failed}」。已完成:${doneList.length ? doneList.join('、') : '(无)'};待办:${SUBSTEPS.filter((k) => st.substeps?.[k] !== 'done' && k !== failed).join('、') || '(无)'}`)
913
+ console.error(`❌ flow done 中断于子步「${failed}」。已完成:${doneList.length ? doneList.join('、') : '(无)'};待办:${remainingSubstepsAtFail({ snapshotSubsteps: st.substeps, doneList, failed }).join('、') || '(无)'}`)
663
914
  console.error(' 重入:修复后重跑同一条命令——已完成子步幂等跳过,从断点续(半态可重入不可假绿:归档子步未完成前 change 仍 active)')
664
915
  }
665
916
 
666
- // ① artifacts:机器稿指纹校验(draft-ledger 在场时三态拒收——标记缺失/哈希失配/手工重锚
667
- // 未审计;AGENT 槽是合法书写面不参与指纹;缺 ledger=not-applicable 放行)
668
- // + 设计记录空槽拒收(2026-09-24 v3 设计记录全档化第一片:盲维四问每跑必答——空槽=未答,
669
- // 「不适用:<理由>」也是答;纯文档检查前置于实测门,秒级失败秒级返工)
917
+ // ① artifacts:工件校验(双轨:v2 纯 markdown=文档↔锚对比+spec 断点机器门;v1 指纹=三态拒收)
918
+ // + 设计记录空槽拒收(v1;v2 的四节空答在 verifyThinDocsV2 内)。纯文档检查前置于实测门,
919
+ // 秒级失败秒级返工。
670
920
  if (st.substeps?.artifacts === 'done') { skip('artifacts') } else {
671
921
  const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
672
- const r = verifyFlowDrafts({ changeDir, change, runtimeRoot })
673
- if (r.applicable && r.violations.length > 0) {
674
- console.error(`❌ 工件校验拒收(机器稿指纹三态):`)
675
- for (const v of r.violations) console.error(` - ${v}`)
676
- reportMidFail('artifacts')
677
- process.exit(1)
678
- }
679
- // 头脑风暴预段设计豁免(2026-09-25-thin-brainstorm-prestage):adopted 变更的 design 是
680
- // brainstorm 人机交互产物(比四节骨架丰富)——承诺以其为准,槽位门不适用
681
- if (st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))) {
682
- console.log('ℹ️ 头脑风暴预段设计在场——豁免 design 四节槽门(adopted_from=brainstorm,设计承诺以 brainstorm design 为准)')
922
+ // 头脑风暴预段设计豁免判定前置(v2 以 skipDesign 透传;v1 分支保留原日志)
923
+ const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
924
+ const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign })
925
+ if (r.applicable && r.schema === 2) {
926
+ // v2 纯 markdown 轨(2026-10-04-thin-docs-v2):violations 拒收 → 漂移 advisory → spec 断点机器门
927
+ if (r.violations.length > 0) {
928
+ console.error(`❌ 工件校验拒收(v2 文档↔锚对比):`)
929
+ for (const v of r.violations) console.error(` - ${v}`)
930
+ reportMidFail('artifacts')
931
+ process.exit(1)
932
+ }
933
+ for (const a of r.advisories) console.warn(`⚠️ [门柱漂移 advisory] ${a}`)
934
+ if (adoptedDesign) console.log('ℹ️ 头脑风暴预段设计在场——豁免 design 四节槽门(adopted_from=brainstorm,设计承诺以 brainstorm design 为准)')
935
+ // spec 断点机器门(FR-06/FR-07,护栏#2 零 prompt 劝说——advisory 断点在 0/12 勾选类事故
936
+ // 实证下无牙;v2 起批准是收口硬前提,autopilot 是用户显式豁免通道)
937
+ if (!st.autopilot && !st.spec_approved) {
938
+ console.error(`❌ spec 断点未批准:本变更(v2 起草)收口需要方案确认留痕`)
939
+ console.error(` 修复:把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
940
+ console.error(` sillyspec flow approve --change ${change}`)
941
+ console.error(` 或用户显式豁免(自主跑到底):重跑 flow start --change ${change} --autopilot(声明留痕)`)
942
+ reportMidFail('artifacts')
943
+ process.exit(1)
944
+ }
683
945
  } else {
684
- const dr = verifyDesignRecordFilled({ changeDir })
685
- if (dr.applicable && dr.emptySlots.length > 0) {
686
- console.error(`❌ 设计记录未作答:design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')})`)
687
- console.error(` 每节至少写一行(小改动可写「不适用:<理由>」)——设计承诺是评审与 FR 对账的锚点,空槽=承诺未落盘`)
688
- console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,从 .runtime/step-guides/ 的指纹缓存可找骨架原文;或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提)`)
946
+ if (r.applicable && r.violations.length > 0) {
947
+ console.error(`❌ 工件校验拒收(机器稿指纹三态):`)
948
+ for (const v of r.violations) console.error(` - ${v}`)
689
949
  reportMidFail('artifacts')
690
950
  process.exit(1)
691
951
  }
952
+ if (adoptedDesign) {
953
+ console.log('ℹ️ 头脑风暴预段设计在场——豁免 design 四节槽门(adopted_from=brainstorm,设计承诺以 brainstorm design 为准)')
954
+ } else {
955
+ const dr = verifyDesignRecordFilled({ changeDir })
956
+ if (dr.applicable && dr.emptySlots.length > 0) {
957
+ console.error(`❌ 设计记录未作答:design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')})`)
958
+ console.error(` 每节至少写一行(小改动可写「不适用:<理由>」)——设计承诺是评审与 FR 对账的锚点,空槽=承诺未落盘`)
959
+ console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,从 .runtime/step-guides/ 的指纹缓存可找骨架原文;或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提)`)
960
+ reportMidFail('artifacts')
961
+ process.exit(1)
962
+ }
963
+ }
692
964
  }
693
965
  mark('artifacts')
694
966
  }
@@ -708,6 +980,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
708
980
  const changedFiles = await attributedChangedFiles()
709
981
  // 哨兵断言(2026-09-25-sentinel-wiring):tasks.md 全勾但零完成证据(区间提交消息标题与正文
710
982
  // 均无 task-NN token 且无对应 review.json)→ 拒收——L0 硬门接线,两道收口同一哨兵(quick 侧同判)。
983
+ let _sentinelNonMirrorTasks = null // 镜像豁免面外的任务数(null=哨兵未跑;0=纯镜像任务面——节奏 advisory 静默)
984
+ let _autopilotTicked = 0 // 机器代勾格数(>0 时勾选节奏门降 advisory——代勾是单拍多格机械写,非 agent 纪律面)
711
985
  // 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
712
986
  // token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
713
987
  // 提交计数不因多行正文失真。
@@ -734,6 +1008,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
734
1008
  if (_autoTicked > 0) {
735
1009
  const { writeAtomicSync } = await import('./fs-atomic.js')
736
1010
  writeAtomicSync(_tasksMdPath, _tasksMd)
1011
+ _autopilotTicked = _autoTicked
1012
+ // 代勾事实持久化(二轮评审 P2-B):重入时 tasks 已被代勾、本拍 _autoTicked=0,
1013
+ // 但首拍代勾写下的单拍跳事件仍在流里——不持久化会把重入误判成 agent 一把勾拒收
1014
+ try { writeFlowState(changeDir, { autopilot_ticked: _autoTicked }) } catch { /* 留痕 best-effort */ }
737
1015
  console.log(`✅ [自动勾选] ${_autoTicked} 个任务有提交证据但未勾——已机器代勾(governance-autopilot)`)
738
1016
  }
739
1017
  }
@@ -743,10 +1021,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
743
1021
  const tasksPath = join(changeDir, 'tasks.md')
744
1022
  if (existsSync(tasksPath)) {
745
1023
  const _logRaw = gitQuiet(cwd, ['log', '--format=%B%x1e', `${st.baseline_commit}..HEAD`])
746
- if (!_logRaw) {
1024
+ if (_logRaw === null || _logRaw === undefined) {
747
1025
  console.log('ℹ️ 哨兵:git log 不可用,跳过(fail-open——空提交组照判会假拦)')
748
- } else {
1026
+ } else { // 空串=区间零提交(哨兵照跑:零提交不豁免——角度 A 空转收口实证)
749
1027
  const commitMessages = String(_logRaw).split('\x1e').map((x) => x.trim()).filter(Boolean)
1028
+ // 证据判据统一(2026-10-07-thin-tasks-v3,镜像豁免/收口代勾退役):tasks.md 是 agent
1029
+ // 工作分解队列,每格勾选一律按 per-task 证据判(提交 token / review.json)——不再读
1030
+ // 机器稿基线做镜像豁免(route-hindsight 基线快照仍供改写比指标,与哨兵解耦)。
750
1031
  const sent = detectFakeCheckCompletion({ changeDir, tasksMd: readFileSync(tasksPath, 'utf8'), commits: commitMessages })
751
1032
  if (sent.status === 'fake') {
752
1033
  console.error(`🚫 哨兵断言拒收:tasks.md 全勾(${sent.checked}/${sent.claimTotal})但 ${sent.missing.length} 个任务零完成证据(区间提交标题与正文均无 token、无 review.json):${sent.missing.join('、')}`)
@@ -755,13 +1036,18 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
755
1036
  reportMidFail('ledger')
756
1037
  process.exit(1)
757
1038
  }
1039
+ _sentinelNonMirrorTasks = sent.claimTotal
758
1040
  if (sent.status === 'complete') {
759
1041
  console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
760
1042
  // 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
761
1043
  // == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
762
1044
  // 这是提醒边干边勾纪律,非假勾)。tasks.md 全程 untracked(未随交付提交)时同款提醒。
1045
+ // 首提锚定(2026-10-07-tick-timing-first-commit):--reverse 全 history 首行=首次提交——
1046
+ // 旧实现 -n 1 取最近一次提交,每任务随交付提交 tasks.md 的渐进形态(首提早于 HEAD)
1047
+ // 被误报一把勾(完整实测实证:历史 0→1→2 渐进勾选仍出警告)。
763
1048
  try {
764
- const tasksIn = gitQuiet(cwd, ['log', '--format=%h', '-n', '1', '--', tasksPath])
1049
+ const _firstRaw = gitQuiet(cwd, ['log', '--reverse', '--format=%h', '--', tasksPath])
1050
+ const tasksIn = _firstRaw ? (String(_firstRaw).split(/\r?\n/).map((x) => x.trim()).filter(Boolean)[0] || null) : null
765
1051
  const lastIn = gitQuiet(cwd, ['log', '--format=%h', '-n', '1'])
766
1052
  if (!tasksIn) {
767
1053
  console.warn(`⚠️ 勾选时点:tasks.md 未随交付提交(untracked 直至归档)——勾选证据链靠归档兜底,git 历史不可回溯;下次收口前 tasks.md 一并显式 pathspec 提交`)
@@ -770,34 +1056,98 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
770
1056
  }
771
1057
  } catch { /* 时点判定 fail-soft */ }
772
1058
  }
773
- // 勾选缺失 advisory(2026-09-25-flow-tick-prototype):有任务行但全未勾(status='none' 且
774
- // claimTotal>0 且 checked===0)而区间有提交 → 记账缺失提醒(不阻断——不勾选不是假勾,是漏账)
775
- if (sent.status === 'none' && sent.claimTotal > 0 && sent.checked === 0 && commitMessages.length > 0) {
776
- console.warn(`⚠️ 任务勾选缺失:tasks.md 有 ${sent.claimTotal} 条任务但一条未勾(区间已有 ${commitMessages.length} 个提交)——`)
777
- console.warn(` 规范动作是把 tasks.md 覆写为自己的工作分解并完成一个单元即勾选(- [ ] → - [x]);请补勾完成项后再收口(本次放行不阻断)`)
1059
+ // 全勾硬门(2026-10-07-allticked-gate-docs-resync,openspec all_done 对齐):tasks.md 是
1060
+ // 完成状态机——有任务行但未全勾即拒收(旧「勾选缺失 advisory 放行」退役:懒路径零勾也
1061
+ // 能归档教会了无视任务面)。出口:逐格 task tick(每格需提交 token 证据——哨兵同门);
1062
+ // 任务面与实际不符可先改写 tasks.md(工作分解归 agent)再逐格勾。token 代勾(governance-
1063
+ // autopilot)已在上方先跑:有证据未勾的格子已代勾,此处拒的纯属零证据未完成面。
1064
+ if (sent.claimTotal > 0 && sent.checked < sent.claimTotal) {
1065
+ console.error(`🚫 任务未全勾(${sent.checked}/${sent.claimTotal})——tasks.md 是完成状态机:完成一件勾一格(每格勾选需对应提交带 task-NN token 或 review.json),全勾后重跑 flow done`)
1066
+ console.error(' 任务面与实际实现路径不符时,先改写 tasks.md(工作分解归你,增删改随意)再逐格勾;查下一任务:sillyspec task tick --change ' + `${change}` + ' --task task-NN(回显进度与指针)')
1067
+ appendTelemetry({ sentinel: 'incomplete-tasks', checked: sent.checked, total: sent.claimTotal })
1068
+ reportMidFail('ledger')
1069
+ process.exit(1)
778
1070
  }
779
1071
  }
780
1072
  }
781
1073
  } catch (e) { console.warn(`⚠️ 哨兵断言失败(fail-open 放行,best-effort): ${(e && e.message) || e}`) }
782
- // 勾选节奏 advisory(2026-09-26-thin-check-cadence):watcher 事件流 task-done 单拍跳 ≥2 格 =
783
- // 一把全勾——per-task 时间戳/进度信号面失真(39 条流零例外实证)。warn 不阻断(对齐上方
784
- // 「任务勾选缺失」advisory 档位:节奏是习惯问题非造假主张,L0 硬门另有其人);观测旁路
785
- // 缺席(无流/无事件/读失败)静默跳过——fail-open,watcher 非真相源。
1074
+ // 勾选节奏门(2026-09-26-thin-check-cadence 起 advisory;2026-09-29-batch-tick-gate 升硬门):
1075
+ // 事件流 task-done 单拍跳 ≥2 格 = 一把全勾——per-task 时间戳/进度信号面失真(39 条流零
1076
+ // 例外实证 + 三次复发 0→6/0→3)。逐格纪律的机器牙齿:任务面存在 → 拒收;
1077
+ // 采样去重(2026-10-07-thin-tasks-v3):task tick 直写的精确事件(source:'task-tick')为
1078
+ // 权威口径,watcher 3s 采样合并跳被去重——CLI 快速连续 tick 不再误伤;观测旁路缺席
1079
+ // (无流/读失败)→ 跳过(fail-open,watcher 非真相源);--allow-batch-tick 显式旁路留痕。
786
1080
  try {
787
1081
  const { readWatcherEvents } = await import('./watcher.js')
788
- const { detectBatchCheckCadence } = await import('./sentinel-assertions.js')
1082
+ const { detectBatchCheckCadence, resolveBatchTickAction } = await import('./sentinel-assertions.js')
789
1083
  const stream = readWatcherEvents({ runtimeRoot, change })
790
1084
  const batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
791
1085
  if (batch) {
1086
+ // 代勾计数取「本拍实时代勾 ∨ 持久化代勾」较大者(二轮 P2-B:重入漂移防护);决策纯函数
1087
+ // 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
1088
+ const _persistedAuto = Number(st?.autopilot_ticked) || 0
1089
+ const _autoN = Math.max(_autopilotTicked, _persistedAuto)
1090
+ const v = resolveBatchTickAction({ batchTick: batch, nonMirrorCount: _sentinelNonMirrorTasks, allowBatchTick, autopilotTicked: _autoN })
792
1091
  const at = Number.isFinite(batch.ts) ? new Date(batch.ts).toLocaleTimeString() : '未知时刻'
793
- console.warn(`⚠️ 勾选节奏:tasks.md 单拍多格勾选(${batch.detail},${at})——未按工作单元逐个勾选`)
794
- console.warn(' 规范动作是完成一个工作单元即勾一格(- [ ] → - [x]);一把全勾使进度信号与 per-task 时间戳失真(本次放行不阻断)')
1092
+ if (v.action === 'bypass') {
1093
+ try { writeFlowState(changeDir, { allow_batch_tick: true }) } catch { /* 留痕 best-effort */ }
1094
+ console.warn(`⚠️ --allow-batch-tick:单拍多格勾选(${batch.detail},${at})硬门显式旁路——留痕 flow-state 与平台时间线`)
1095
+ } else if (v.action === 'advisory') {
1096
+ const why = v.reason === 'autopilot-ticked'
1097
+ ? `机器代勾 ${_autoN} 格(governance-autopilot 单拍机械写,非 agent 纪律面;本拍 ${_autopilotTicked}/持久化 ${_persistedAuto})`
1098
+ : '哨兵任务面未知(fail-open 防误拒)'
1099
+ console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——${why},降级提醒不拒`)
1100
+ } else if (v.action === 'reject') {
1101
+ console.error(`🚫 单拍勾选拒收:事件流观测到一拍勾选 ${batch.detail}(${at})——勾选纪律要求逐格(做一件→勾一格→下一个),勾选任务 ${_sentinelNonMirrorTasks} 格`)
1102
+ console.error(' 出口:①节奏违例已既成——认知后重跑带 --allow-batch-tick 显式留痕过门(平台时间线可见旁路);②疑观测误判→sillyspec doctor 核对事件流')
1103
+ appendTelemetry({ sentinel: 'batch-tick', jump: batch.detail })
1104
+ reportMidFail('ledger')
1105
+ process.exit(1)
1106
+ }
795
1107
  }
796
- } catch { /* 节奏 advisory best-effort:读流失败静默 */ }
797
- const gate = await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧已有带 baseline 的哨兵,quick 侧区间不可靠——单判不双判 */ })
1108
+ } catch { /* 节奏门 best-effort:读流失败静默(fail-open——观测缺席不阻断) */ }
1109
+ const gate = await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧哨兵带 baseline 区间,quick 侧区间不可靠——单判不双判 */ })
798
1110
  if (gate && gate.action === 'fail') {
799
1111
  console.error(`❌ 测试门 FAIL(整单 FAIL——实测失败/超时=失败,不继续 distill/归档):`)
800
1112
  console.error(` ${gate.reason || gate.message || JSON.stringify(gate)}`)
1113
+ // FAIL 三件套(2026-09-28-split-guard-and-gate-report,P6):失败行样本 + 结果文件全路径 +
1114
+ // 可粘贴重放的批命令——排障不再手翻 .runtime(快照模式回拷后同样可读)。
1115
+ try {
1116
+ const t = gate.test
1117
+ if (t && t.resultPath && existsSync(t.resultPath)) {
1118
+ const tr = JSON.parse(readFileSync(t.resultPath, 'utf8'))
1119
+ const rem = tr.failure_remaining || []
1120
+ if (rem.length > 0) {
1121
+ console.error(` 失败行(前 5,完整清单在结果文件 failure_remaining):`)
1122
+ for (const l of rem.slice(0, 5)) console.error(` - ${String(l).trim().slice(0, 140)}`)
1123
+ }
1124
+ for (const m of tr.modules || []) {
1125
+ if (m.status && m.status !== 'passed' && m.command) console.error(` 重放(快照内命令,主仓同命令可复跑):${m.command}`)
1126
+ }
1127
+ console.error(` 结果文件:${t.resultPath}`)
1128
+ }
1129
+ } catch { /* 三件套 best-effort:读不到不阻断 FAIL 主输出 */ }
1130
+ // lint FAIL 件套(坑 flowdone-lint-fail-no-output,2026-10-03 实证:quick-audit 的
1131
+ // runVerifyLintCheck 全程静默、printVerifyLintCheck 未在 flow 路径调用——「命令与输出
1132
+ // 尾部见上」名不副实,agent 只能盲猜或直调同参复现)。对齐 test 三件套:命令 + 输出
1133
+ // 尾部 + 失败文件 + 结果文件(lint 结果已由 persistLintResult 并入 test-result.json
1134
+ // 或独立落盘,kind: 'lint')。
1135
+ try {
1136
+ const l = gate.lint
1137
+ if (l && l.status === 'failed') {
1138
+ console.error(` lint 命令:${l.command || '(未知)'}${l.reason ? `(${l.reason})` : ''}`)
1139
+ const tail = String(l.outputTail || '')
1140
+ if (tail) {
1141
+ const lines = tail.split('\n').filter((x) => x.trim())
1142
+ console.error(` lint 输出尾部(后 ${Math.min(lines.length, 15)} 行):`)
1143
+ for (const line of lines.slice(-15)) console.error(` | ${line.slice(0, 160)}`)
1144
+ }
1145
+ if (Array.isArray(l.failureFiles) && l.failureFiles.length > 0) {
1146
+ console.error(` lint 失败文件(前 10):${l.failureFiles.slice(0, 10).join('、')}${l.failureFiles.length > 10 ? ' 等' : ''}`)
1147
+ }
1148
+ if (l.resultPath) console.error(` lint 结果文件:${l.resultPath}`)
1149
+ }
1150
+ } catch { /* lint 件套 best-effort */ }
801
1151
  // 失败触发升级(R7 切片四 / FR-10 / 护栏#4:不依赖 agent 主动)——剩余流程按厚档走
802
1152
  writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `verify 实测失败(${gate.reason || 'test fail'})——失败自动升厚` })
803
1153
  console.error(' ⬆️ 已自动升厚档(tier=thick):重入修复后剩余流程按厚档语义(归档不跳过 plan.md 校验)')
@@ -815,7 +1165,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
815
1165
  console.log(`🧾 实测面对账 — ${gateSummaryText}`)
816
1166
  try { writeFlowState(changeDir, { gate_summary: gateSummaryText }) } catch { /* 存档 best-effort */ }
817
1167
  // FR 腐烂 suspect(2026-09-25-thin-fr-inject-parity):quick-done 钩子迁轻量道(quick 退役后
818
- // 原侧悬空)——归属文件面触达域的 active FR 打待复核标记,下次知识注入带 ⚠️。advisory 不阻断。
1168
+ // 原侧悬空)——归属文件面触达域的 active FR 出收口 advisory 与遥测(2026-09-29 起不落盘)。advisory 不阻断。
819
1169
  try {
820
1170
  const rot = await rotSuspectFlow({ specBase, change, changeDir, files: changedFiles })
821
1171
  if (rot.warn) console.warn(rot.warn)
@@ -860,6 +1210,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
860
1210
  _li++
861
1211
  }
862
1212
  }
1213
+ // v2 纯文本绑定行(2026-10-04-thin-docs-v2):`FR-NN: (待填…)` → 测试结果补全
1214
+ for (let _li = 0; _li < _lines.length; _li++) {
1215
+ const _bm = _lines[_li].match(/^(FR-\d{2}: )(待填[^)]*)\s*$/)
1216
+ if (_bm) {
1217
+ _lines[_li] = _bm[1] + _bindingLine
1218
+ _autoFilled++
1219
+ }
1220
+ }
863
1221
  if (_autoFilled > 0) {
864
1222
  const { writeAtomicSync } = await import('./fs-atomic.js')
865
1223
  writeAtomicSync(_reqPath, _lines.join('\n'))
@@ -881,7 +1239,62 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
881
1239
  // 退役后审计件挂变更自身。范围=归属收窄后的本变更文件面(他侧声明剔除、含工作树未提交与
882
1240
  // untracked 自拼 hunk),基=baseline,时点=done 冻结。fail-soft:失败只告警不阻断归档、
883
1241
  // 不标 done(重入 done 重试)。
884
- if (st.substeps?.patch === 'done') { skip('patch') } else {
1242
+ let driftRefreeze = false
1243
+ if (st.substeps?.patch === 'done') {
1244
+ // 处置漂移检测(2026-10-05-disposition-refreeze-drift,主清单 P1):冻结后又有本变更交付
1245
+ // 提交时,幂等跳过会把 change.patch/review.json 停在处置前时点(归档审计件缺处置面,
1246
+ // 2026-10-05-flowdone-lintfail-output 收编活体复现)。漂移 → 自动重冻结(等价 --refreeze)
1247
+ // + review 已有结论则隔离旧 review.json(结论对着旧冻结面不作数)并重置标记强制重评。
1248
+ try {
1249
+ const patchMeta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
1250
+ const { detectPatchDrift } = await import('./flow-parity.js')
1251
+ const drift = detectPatchDrift({ cwd, change, freezeHead: patchMeta.head })
1252
+ if (drift.drifted) {
1253
+ console.warn(`⚠️ 审计时点漂移:patch 冻结(${String(patchMeta.head || '').slice(0, 8)})后另有 ${drift.ownCommits.length} 个本变更交付提交(${drift.ownCommits.slice(0, 3).join(';')}${drift.ownCommits.length > 3 ? ' 等' : ''})——幂等跳过会把审计件停在处置前时点`)
1254
+ // 隔离先行(评审处置 P3:时间戳槽位防跨代覆盖——单槽 .superseded 在 Windows renameSync
1255
+ // 同名碰撞会抛错;隔离失败则整体退回现状幂等跳过(fail-safe:漂移处理要么完整交付要么
1256
+ // 不动,不留半套状态),指引人工处置后重跑)
1257
+ const reviewPath = join(changeDir, 'review.json')
1258
+ let quarantineOk = true
1259
+ if (existsSync(reviewPath)) {
1260
+ // 锚定保留判定(2026-10-06-review-anchor-and-negation 实测竞态:复审对着当前 HEAD
1261
+ // 做的 PASS 结论落盘 26 秒后被「重冻结时刻在场=过期」口径误隔离):reviewedAgainst
1262
+ // 命中当前 HEAD = 评审对象即最新交付面,结论有效——保留不隔离、review 标记不重置。
1263
+ // 缺省/读失败/格式非法按未锚定处理,隔离行为不变(fail-safe 宁可多评)。
1264
+ let anchoredToHead = false
1265
+ try {
1266
+ const { reviewAnchoredToHead } = await import('./flow-review.js')
1267
+ anchoredToHead = reviewAnchoredToHead(reviewPath, drift.head)
1268
+ } catch { /* 锚定判定失败按未锚定处理 */ }
1269
+ if (anchoredToHead) {
1270
+ console.log(` review.json 已锚定当前 HEAD(reviewedAgainst 命中 ${String(drift.head || '').slice(0, 8)})——评审对象即最新交付面,结论保留不隔离`)
1271
+ } else {
1272
+ const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1273
+ try {
1274
+ renameSync(reviewPath, `${reviewPath}.superseded-${ts}`)
1275
+ console.warn(` 旧 review.json 已隔离为 review.json.superseded-${ts}(评审结论对着旧冻结面)——重新执行评审任务书后再收口`)
1276
+ } catch (e) {
1277
+ quarantineOk = false
1278
+ console.error(` ⚠️ 旧 review.json 隔离失败(${e.message})——本次保持现状幂等跳过,手动删除/改名 ${reviewPath} 后重跑(将自动重冻结+重评)`)
1279
+ }
1280
+ }
1281
+ }
1282
+ if (quarantineOk) {
1283
+ driftRefreeze = true
1284
+ if (st.substeps?.review === 'done' && !existsSync(reviewPath)) {
1285
+ // 双故障边界(评审 P3 处置披露):writeFlowState 盘写失败时内存重置保本运行正确;
1286
+ // 极端双故障(隔离成功+盘写失败)下轮残留 done 且 review.json 缺席 → review 子步
1287
+ // backfill 按 exempt 误读——该窄路径未测(需 I/O 故障注入,design 边界披露)
1288
+ try { writeFlowState(changeDir, { substeps: { review: null } }) } catch { /* 见上边界注释 */ }
1289
+ if (st.substeps) st.substeps.review = null
1290
+ if (!existsSync(reviewPath)) console.warn(' review 豁免口径对着旧冻结面——review 子步标记已重置,本次重新定档')
1291
+ }
1292
+ }
1293
+ }
1294
+ } catch { /* 漂移检测失败按现状幂等跳过(fail-safe 不新造阻断面) */ }
1295
+ if (!driftRefreeze) skip('patch')
1296
+ }
1297
+ if (st.substeps?.patch !== 'done' || driftRefreeze) {
885
1298
  let patchOk = false
886
1299
  try {
887
1300
  // patch 面 = 本变更可归属变化(2026-09-25-thin-patch-scope-fix 收窄:dirty 全扫面会把并行
@@ -993,7 +1406,22 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
993
1406
  patchOk = true
994
1407
  } else {
995
1408
  const { buildFrozenPatch, collectNumstatByPath } = await import('./scope-audit.js')
996
- const frozen = buildFrozenPatch(cwd, ownFiles, { baseRef: st.baseline_commit })
1409
+ // 双口径冻结(2026-09-28-split-guard-and-gate-report 评审 P2 清偿):交付面(committed)
1410
+ // 用 baseRef..HEAD 提交区间 diff——共享主仓里并行会话对同文件的未提交 hunk 不再漏进
1411
+ // 冻结件;治理工件目录(changeDirFiles,本变更独占无他会话面)保留工作树口径(未提交
1412
+ // 的槽位作答/勾选要进审计件)。重叠文件归提交区间,避免双 hunk。
1413
+ const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
1414
+ const toPosix = (p) => String(p).replace(/\\/g, '/')
1415
+ // 复审 P2/P3a 清偿:工作树口径集 = 治理工件目录(含已提交后又改的槽位/勾选——工作树
1416
+ // diff 能捕捉提交后编辑)∪ 独占树未提交交付(dirtyAdded);其余交付面走提交区间。
1417
+ // 排除项与 ownFiles 同款(change.patch/change-patch.json)——否则上一轮冻结件作为
1418
+ // untracked 新文件被全文自嵌入(自引用循环:旧 patch 内容混进新 patch,三评 P1 根因)。
1419
+ const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json'))
1420
+ const worktreeSet = new Set([...dirFilesForPatch, ...((freeze.dirtyAdded) || [])].map(toPosix))
1421
+ const committedFace = freeze.files.map(toPosix).filter((f) => !worktreeSet.has(f))
1422
+ const frozenCommitted = buildFrozenPatch(cwd, committedFace, { baseRef: st.baseline_commit, headRef: headCommit })
1423
+ const frozenDir = buildFrozenPatch(cwd, [...worktreeSet], { baseRef: st.baseline_commit })
1424
+ const frozen = [frozenCommitted, frozenDir].filter((p) => typeof p === 'string' && p).join('\n') || null
997
1425
  const stats = collectNumstatByPath(cwd, ownFiles, { baseRef: st.baseline_commit })
998
1426
  let additions = 0
999
1427
  let deletions = 0
@@ -1092,8 +1520,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1092
1520
  reviewOutcome = { required: false, sampled: false, verdict: 'exempt' }
1093
1521
  mark('review')
1094
1522
  } else if (!existsSync(join(changeDir, 'review.json'))) {
1523
+ // 任务书标注评审对象 HEAD(2026-10-06-review-anchor-and-negation):评审员照抄进
1524
+ // review.json.reviewedAgainst——处置提交后的重入重跑据此保留「对着最新面的刚完成评审」,
1525
+ // 不再被漂移重冻结误隔离(实测竞态修复)。rev-parse 失败传 null(任务书退引导形态)。
1526
+ let taskbookHead = null
1527
+ try { const h = gitQuiet(cwd, ['rev-parse', 'HEAD']); if (h && /^[0-9a-f]{7,40}$/i.test(String(h).trim())) taskbookHead = String(h).trim() } catch { /* best-effort */ }
1095
1528
  console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
1096
- console.log(renderReviewerTaskbook({ change, changeDir }))
1529
+ console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead }))
1097
1530
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
1098
1531
  appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
1099
1532
  reportMidFail('review')
@@ -1128,6 +1561,22 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1128
1561
  }
1129
1562
  }
1130
1563
 
1564
+ // ②d hindsight:事后闭环指标接线(FR-02 / D-005,2026-09-28-unclear-req-to-brainstorm)——
1565
+ // patch/review 之后、archive 搬走 changeDir 之前:review.json(评审子步已定在场与否)、首版
1566
+ // 快照 vs 终稿、verify-runs 记录面 → 封闭面指标四元组(diff 比例与计数,零语义判定 D-003);
1567
+ // 任一超阈 → per-repo 标记落库,下次 flow start 点名提示(「疑似」非定罪可无视)。
1568
+ // best-effort:异常仅 warn 不阻断收口(断点续跑重入本块幂等——标记整文件覆盖同值)。
1569
+ try {
1570
+ const { computeHindsightMetrics, markHindsight } = await import('./route-hindsight.js')
1571
+ let reviewJson = null
1572
+ try { reviewJson = JSON.parse(readFileSync(join(changeDir, 'review.json'), 'utf8')) } catch { /* 评审豁免/缺件 → null(盲维计 0) */ }
1573
+ const metrics = computeHindsightMetrics({ changeDir, reviewJson, flowState: st })
1574
+ const marked = markHindsight({ cwd, specBase, change, metrics })
1575
+ if (marked.marked) {
1576
+ console.warn(`🕰️ route-hindsight 标记:本变更疑似该走预段未走(${marked.reasons.join(';')})——下次 flow start 将点名提示(过程形态信号非定罪)`)
1577
+ }
1578
+ } catch (e) { console.warn(`⚠️ route-hindsight 指标接线失败(best-effort 不阻断收口): ${(e && e.message) || e}`) }
1579
+
1131
1580
  // ③ probes:thin 轻量跑无 verify-result 骨架——探针产物面(probe1-8 事实核验)由 flow done
1132
1581
  // 裁决自含(测试门+工件指纹);升厚(tier=thick)时探针链由 run verify 的既有 --init --draft
1133
1582
  // 产物面承接(第 3 批资产复用,不重做)。本子步记账占位:thin=not-applicable 直过。
@@ -1327,9 +1776,42 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1327
1776
  }
1328
1777
 
1329
1778
  /** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
1330
- export async function cmdFlow(args, cwd, specDir = null) {
1779
+ /**
1780
+ * flow approve(2026-10-04-thin-docs-v2 FR-06):spec 断点用户批准留痕——用户确认 FR+design
1781
+ * 方案后由**用户**运行(不是 agent 代跑);写 flow-state {spec_approved,spec_approved_at,
1782
+ * spec_approved_by}。flow done 的 v2 机器门消费该证据(未批准且未声明 autopilot → 拒收)。
1783
+ * 诚实边界:同机 agent 理论上也能跑本命令——门的价值是仪式+留痕(谁在何时批的)+评审抽查
1784
+ * 兜底,非绝对防伪;--by 显式署名供平台/脚本侧调用。
1785
+ */
1786
+ async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
1787
+ const changeDir = join(specBase, 'changes', change)
1788
+ if (!existsSync(changeDir)) {
1789
+ console.error(`❌ 变更不存在:${change}(flow approve 在 flow start 之后运行)`)
1790
+ process.exit(2)
1791
+ }
1792
+ let approvedBy = by
1793
+ if (!approvedBy) {
1794
+ try {
1795
+ const { resolveSessionIdentity } = await import('./progress.js')
1796
+ approvedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
1797
+ } catch { approvedBy = 'user' }
1798
+ }
1799
+ const at = new Date().toISOString()
1800
+ writeFlowState(changeDir, { spec_approved: true, spec_approved_at: at, spec_approved_by: approvedBy })
1801
+ console.log(`✅ spec 断点已批准(change=${change})`)
1802
+ console.log(` 批准人:${approvedBy}|时间:${at}`)
1803
+ console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:编辑 flow-state.yaml 删除 spec_approved 三字段。`)
1804
+ }
1805
+
1806
+ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
1331
1807
  const sub = args[0] || ''
1332
1808
  const rest = args.slice(1)
1809
+ // 全局 --json 透传(2026-10-06-flow-status-json):index.js 顶层把 --json 从 filteredArgs
1810
+ // 剥进全局变量,flow 族此前在真实 CLI 入口下永远收不到该 flag(in-process 直调 cmdFlow 带
1811
+ // --json 才生效)——cmdFlowStart 的 json 信封成了死代码、status 无机器可读面。统一入口
1812
+ // 形状:第 4 参 opts.json 由 index.js 传入,与 args 内残余 --json(直调/测试路径)取或,
1813
+ // 两路同语义不分叉。
1814
+ const jsonGlobal = opts.json === true
1333
1815
  // 平台参数面(2026-09-25-thin-platform-args,平台侧三子代理核对驱动):specBase 统一走
1334
1816
  // resolvePlatformSpecDir——显式 --spec-dir/--spec-root > .sillyspec-platform.json 指针(fail-closed,
1335
1817
  // 指针失效报错不静默回退本地防状态分裂)> 本地。此前 flow 族零平台支持:--spec-dir 是 ENOENT
@@ -1361,26 +1843,82 @@ export async function cmdFlow(args, cwd, specDir = null) {
1361
1843
  process.exit(exitCode)
1362
1844
  }
1363
1845
  }
1846
+ // ── agent 会话日志登记 + 上报(2026-09-29-flow-agent-log-report,对齐 run 族)──
1847
+ // runCommand 入口统一调 recordAgentLogInvocation(run/command.js 同款)——run 族全量覆盖;
1848
+ // flow 走本独立入口此前从未接入,轻量变更的本地会话路径不上报平台(平台只见 CLI 阶段信息,
1849
+ // 看不到 agent 实际执行日志)。各子命令 change 解析后 best-effort 调用:探测 agent 环境
1850
+ // (Claude Code / Codex / ZCode transcript / SILLYSPEC_AGENT_LOG 覆盖)登记
1851
+ // <runtimeRoot>/agent-session-log.json 并 REST 上报(POST /api/agent-logs,own 打标/推送
1852
+ // 收敛/互斥语义复用同实现)。context.changeKey=flow change 名(flow 无 quick 会话概念,
1853
+ // quickId 恒空);hubSessionId 走 env SILLYHUB_SESSION_ID(daemon 注入通道,与 run 同源)。
1854
+ // 推送上限 5s(PUSH_TIMEOUT_MS)、失败静默留底——best-effort,绝不阻断协议面。
1855
+ const reportAgentLog = async (changeKey, subName) => {
1856
+ try {
1857
+ const { recordAgentLogInvocation } = await import('./agent-session-log.js')
1858
+ const hubSessionIdEnv = typeof process.env.SILLYHUB_SESSION_ID === 'string' ? process.env.SILLYHUB_SESSION_ID.trim() : ''
1859
+ await recordAgentLogInvocation({
1860
+ cwd,
1861
+ platformOpts: runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {},
1862
+ specBase,
1863
+ context: { hubSessionId: hubSessionIdEnv || null, changeKey: changeKey || null, quickId: null },
1864
+ // 只记 flag 名不记值(对齐 run 口径:--input 等 flag 值是 agent 工作文本,不进产物)
1865
+ command: [subName, ...rest.filter(t => typeof t === 'string' && t.startsWith('--'))].join(' '),
1866
+ })
1867
+ } catch { /* best-effort:登记失败不影响 flow 协议面 */ }
1868
+ }
1364
1869
  if (sub === 'start') {
1365
- const change = getFlag('--change') || `flow-${new Date().toISOString().slice(0, 10)}-${Math.random().toString(16).slice(2, 6)}`
1870
+ const change = getFlag('--change') || `${new Date().toISOString().slice(0, 10)}-flow-${Math.random().toString(16).slice(2, 6)}`
1366
1871
  validateChangeName(change)
1872
+ // 变更名日期前缀门禁(对齐 run/command.js 净新建门与 change-rename 门,brainstorm step6
1873
+ // 规则 CLI 化):轻量道此前无此门,roadmap-copy-purge 实证无前缀名被照单物化入档,打乱
1874
+ // 归档字典序时间线。只拦净新建(changes/<名> 与 changes/archive/<名> 均不存在);恢复/
1875
+ // 头脑风暴收编/平台 writer 预建空目录均目录在场不追诉(旧无前缀存量照常可续跑)。
1876
+ if (!existsSync(join(specBase, 'changes', change)) && !existsSync(join(specBase, 'changes', 'archive', change))) {
1877
+ try {
1878
+ assertDatedChangeName(change)
1879
+ } catch (e) {
1880
+ console.error(`❌ ${e.message}`)
1881
+ // 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
1882
+ console.error(' 重试(--input 引号内换行合法,可照抄):')
1883
+ console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<动机与背景>')
1884
+ console.error('')
1885
+ console.error(' 成功标准:')
1886
+ console.error(' - <可验证标准>"')
1887
+ process.exit(2) // 用法错(净新建变更名缺日期前缀/格式非法)→ exit 2
1888
+ }
1889
+ }
1890
+ await reportAgentLog(change, sub)
1367
1891
  return cmdFlowStart({
1368
1892
  change,
1369
1893
  input: getFlag('--input') || undefined,
1894
+ title: getFlag('--title') || null,
1370
1895
  thick: hasFlag('--thick'),
1371
1896
  withTasks: hasFlag('--with-tasks'),
1372
1897
  reviewForce: hasFlag('--review') ? true : hasFlag('--no-review') ? false : null,
1898
+ autopilot: hasFlag('--autopilot'),
1373
1899
  cwd, specBase,
1374
- json: hasFlag('--json'),
1900
+ json: hasFlag('--json') || jsonGlobal,
1375
1901
  })
1376
1902
  }
1903
+ if (sub === 'approve') {
1904
+ const change = getFlag('--change')
1905
+ if (!change) { console.error('❌ flow approve 需 --change <名>'); process.exit(2) }
1906
+ validateChangeName(change)
1907
+ await reportAgentLog(change, sub)
1908
+ return cmdFlowApprove({ change, cwd, specBase, by: getFlag('--by') || null })
1909
+ }
1377
1910
  if (sub === 'status') {
1378
1911
  // 三断点配套(2026-09-25-flow-checkpoints):随时可查当前变更阶段/槽位/子步进度
1379
1912
  const change = getFlag('--change')
1380
1913
  if (!change) { console.error('❌ flow status 需 --change <名>'); process.exit(2) }
1381
1914
  validateChangeName(change)
1915
+ await reportAgentLog(change, sub)
1382
1916
  const changeDir = join(specBase, 'changes', change)
1383
1917
  const st = readFlowState(changeDir)
1918
+ // --json(2026-10-06-flow-status-json):机器可读单对象输出——事实与人类渲染同源同判定,
1919
+ // 退出码同点(missing=1,其余=0),程序化消费方不再 fragile 文本匹配。
1920
+ // 入口双路:jsonGlobal 来自 index.js 全局透传(真实 CLI),hasFlag 覆盖直调/测试路径
1921
+ const json = jsonGlobal || hasFlag('--json')
1384
1922
  if (!st) {
1385
1923
  // 归档检测:归档后目录搬至 changes/archive/<日期>-<名>(原目录不存在)——查 archive 目录
1386
1924
  const archiveDir = join(specBase, 'changes', 'archive')
@@ -1388,54 +1926,110 @@ export async function cmdFlow(args, cwd, specDir = null) {
1388
1926
  try {
1389
1927
  if (existsSync(archiveDir)) isArchived = readdirSync(archiveDir).some((e) => e === change) // 精确匹配(fr-governance-sweep:归档目录名恒等 change 名,includes 子串会把 flow-check 误命中 flow-checkpoints)
1390
1928
  } catch { /* best-effort */ }
1391
- if (isArchived) {
1392
- console.log(`📦 ${change}:已归档`)
1393
- } else if (existsSync(changeDir)) {
1394
- console.log(`📁 ${change}:变更目录在场但无 flow-state(可能头脑风暴预段产物,尚未进入轻量变更)`)
1395
- } else {
1396
- console.log(`❓ ${change}:变更不存在`)
1397
- }
1929
+ // 前置形态先归一(archived/dir-no-state/missing)——--json 与人类渲染共用同一判定,防两路径各算各的漂移
1930
+ const form = isArchived ? 'archived' : (existsSync(changeDir) ? 'dir-no-state' : 'missing')
1931
+ if (json) console.log(JSON.stringify({ change, status: form }))
1932
+ else if (form === 'archived') console.log(`📦 ${change}:已归档`)
1933
+ else if (form === 'dir-no-state') console.log(`📁 ${change}:变更目录在场但无 flow-state(可能头脑风暴预段产物,尚未进入轻量变更)`)
1934
+ else console.log(`❓ ${change}:变更不存在`)
1935
+ // exit 1(2026-10-05-flow-tail-polish):查询目标缺失=运行错——exit 0 时脚本无法区分
1936
+ // 「存在但无进度」与「不存在」;在场两形态(已归档/目录在场)仍走上方形态路径 exit 0。
1937
+ // --json 与人类路径同点退出(分叉会让脚本消费方无法复用退出码判定)
1938
+ if (form === 'missing') process.exit(1)
1398
1939
  return
1399
1940
  }
1400
1941
  const subDone = SUBSTEPS.filter((k) => st.substeps?.[k] === 'done')
1401
1942
  const subLeft = SUBSTEPS.filter((k) => st.substeps?.[k] !== 'done')
1943
+ // 变更标题(2026-10-06-flow-status-title):flow start/--title 写入进度库,status 只读回显
1944
+ // (getChangeTitle 前置判 DB 在场——只读查询不建库;读取失败按无标题渲染,输出与现状一致)
1945
+ let changeTitle = null
1946
+ try {
1947
+ const { ProgressManager } = await import('./progress.js')
1948
+ changeTitle = new ProgressManager({ specDir: specBase }).getChangeTitle(cwd, change)
1949
+ } catch { /* 标题 best-effort */ }
1402
1950
  // 槽位快检
1403
1951
  let designFilled = false, frFilled = false, bindingsFilled = 0, bindingsTotal = 0
1404
1952
  try {
1405
1953
  const dText = readFileSync(join(changeDir, 'design.md'), 'utf8')
1406
- designFilled = Array.from(dText.matchAll(/<!--AGENT:槽\d+[^\n]*-->\n(\S)/g)).length >= 3
1954
+ // 双格式:v1 AGENT 槽计数;v2 纯 markdown 按节「有实质作答」判(复用 flow-review 的
1955
+ // v2 节读取——问题原文行剥离;仅标题在场不算已填,防 spec 相位误跳「②执行」)
1956
+ if (/<!--AGENT:槽\d+/.test(dText)) {
1957
+ designFilled = Array.from(dText.matchAll(/<!--AGENT:槽\d+[^\n]*-->\n(\S)/g)).length >= 3
1958
+ } else {
1959
+ try {
1960
+ const { readV2SectionAnswer } = await import('./flow-review.js')
1961
+ const { DESIGN_QUESTIONS } = await import('./flow-draft.js')
1962
+ designFilled = DESIGN_QUESTIONS.sections.filter((s) => readV2SectionAnswer(dText, s.heading, DESIGN_QUESTIONS)).length >= 3
1963
+ } catch { designFilled = false }
1964
+ }
1407
1965
  } catch { /* 无 design */ }
1408
1966
  try {
1409
1967
  const rText = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
1410
1968
  frFilled = /### FR-\d+:/.test(rText.replace(/<!--[\s\S]*?-->/g, ''))
1411
- bindingsTotal = (rText.match(/<!--AGENT:测试绑定FR-\d+/g) || []).length
1412
- bindingsFilled = (rText.match(/<!--AGENT:测试绑定FR-\d+[^\n]*-->\n\S/g) || []).length
1969
+ if (/<!--AGENT:测试绑定FR-\d+/.test(rText)) {
1970
+ bindingsTotal = (rText.match(/<!--AGENT:测试绑定FR-\d+/g) || []).length
1971
+ bindingsFilled = (rText.match(/<!--AGENT:测试绑定FR-\d+[^\n]*-->\n\S/g) || []).length
1972
+ } else {
1973
+ // v2 纯文本行:`FR-NN: 内容`((待填=未答)
1974
+ const _rows = [...rText.matchAll(/^(FR-\d{2}):\s*(.*)$/gm)]
1975
+ bindingsTotal = _rows.length
1976
+ bindingsFilled = _rows.filter((m) => m[2].trim() && !m[2].trim().startsWith('(待填')).length
1977
+ }
1413
1978
  } catch { /* 无 requirements */ }
1414
1979
  // 阶段推断
1415
1980
  let phase = '① spec(填 FR + design 槽)'
1416
1981
  if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) phase = '② 执行(写代码跑测试)→ ①卡点:spec 摘要给用户确认'
1417
1982
  if (subDone.includes('ledger')) phase = '③ 归档(flow done 收口)'
1418
- // 勾选提醒(2026-09-26-tick-loop-nudge,R19 实证:起点简报一次性指令几小时后失效,OS 的
1419
- // 边干边勾靠 Guardrails 常驻干活循环——本行把提醒带进 agent 中途必经的 status 面):
1420
- // ②执行阶段且有完成迹象(区间已有提交)但勾选滞后 → 一行轻推(不重复刷——只在滞后时出现)。
1421
- let tickNudge = null
1983
+ // 任务心跳(2026-09-29-flow-task-heartbeat + d007-incontext 纠偏):②执行阶段把「下一个任务」
1984
+ // 交给机器指——当场重读 tasks.md(唯一进度源)取第一个未勾行 + 进度。进度推进本身零 CLI
1985
+ // (做一件→勾一格→继续下一条,D-007 中间零协议必需交互);本面是自愿查看/恢复面——用户
1986
+ // 问进度、断点续跑时一次调用拿到指针。全勾改指 flow done;勾选证据口径不变(哨兵逐 task)。
1987
+ let heartbeat = null
1422
1988
  try {
1423
1989
  if (phase.startsWith('②')) {
1424
1990
  const t = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
1425
1991
  const c = (t.match(/^- \[x\]/gm) || []).length
1426
1992
  const tot = (t.match(/^- \[( |x)\]/gm) || []).length
1427
- const commits = st.baseline_commit ? (gitQuiet(cwd, ['rev-list', '--count', `${st.baseline_commit}..HEAD`]) || '0').toString().trim() : '0'
1428
- if (tot > 0 && c < tot && parseInt(commits, 10) > 0) {
1429
- tickNudge = ` ✅ 勾选提醒:已完成单元请顺手勾(${c}/${tot},区间已有 ${commits} 提交——边干边勾是哨兵证据面与进度锚,勿攒到收口一把勾)`
1993
+ const next = t.split(/\r?\n/).find((l) => /^- \[ \] (task-\d+):/.test(l))
1994
+ if (tot > 0 && next) {
1995
+ const m = next.match(/^- \[ \] (task-\d+):\s*(.*)$/)
1996
+ heartbeat = [
1997
+ ` ⏭️ 下一任务:${m[1]} ${String(m[2] || '').slice(0, 60)}`,
1998
+ ` ✅ 进度:${c}/${tot}——边干边勾:做一件 → 勾一格(- [ ] → - [x])→ 继续下一条(tasks.md 是进度源,勿攒一把勾;收口哨兵逐 task 核证据)。本面为自愿查看/恢复面——中间零协议必需交互(D-007)`,
1999
+ ]
2000
+ } else if (tot > 0 && c >= tot) {
2001
+ // 全勾判定用 c>=tot 而非「找不到未勾 task-NN 行」——行形态漂移(未勾行缺 task-NN
2002
+ // 前缀)时宁可静默不出心跳,不误刷「任务全勾」催 flow done(评审 P3)
2003
+ heartbeat = [` ✅ 任务全勾(${c}/${tot})——跑 flow done 收口(哨兵逐 task 核对提交 token/review.json 证据)`]
1430
2004
  }
1431
2005
  }
1432
- } catch { /* 提醒 fail-soft */ }
2006
+ } catch { /* 心跳 fail-soft */ }
2007
+ // 任务勾选预计算(--json 与人类渲染共用同源;正则与原内联 IIFE 逐字一致;hasTasks 保住
2008
+ // 「缺 tasks.md 时不渲染该行」的现状——人类输出不得因本变更变形)
2009
+ let tasksChecked = 0, tasksTotal = 0, hasTasks = false
2010
+ try {
2011
+ const t = readFileSync(join(changeDir, 'tasks.md'), 'utf8')
2012
+ hasTasks = true
2013
+ tasksChecked = (t.match(/^- \[x\]/gm) || []).length
2014
+ tasksTotal = (t.match(/^- \[( |x)\]/gm) || []).length
2015
+ } catch { /* 无 tasks.md */ }
2016
+ if (json) {
2017
+ console.log(JSON.stringify({
2018
+ change, status: 'active', phase, title: changeTitle,
2019
+ designFilled, frFilled, bindingsFilled, bindingsTotal,
2020
+ tasksChecked, tasksTotal,
2021
+ substeps: subDone, substepsTotal: SUBSTEPS.length,
2022
+ ...(st.legacy_fallback ? { legacyFallback: true } : {}),
2023
+ }))
2024
+ return
2025
+ }
1433
2026
  console.log([
1434
2027
  `📋 ${change}`,
2028
+ ...(changeTitle ? [` 标题:${changeTitle}`] : []),
1435
2029
  ` 阶段:${phase}`,
1436
2030
  ` design 槽:${designFilled ? '✅ 已填' : '⬜ 未填'}|FR 区:${frFilled ? '✅ 已填' : '⬜ 未填'}|绑定槽:${bindingsFilled}/${bindingsTotal}`,
1437
- (() => { try { const t = readFileSync(join(changeDir, 'tasks.md'), 'utf8'); const c = (t.match(/^- \[x\]/gm) || []).length; const tot = (t.match(/^- \[( |x)\]/gm) || []).length; return ` 任务勾选:${c}/${tot}` } catch { return null } })(),
1438
- tickNudge,
2031
+ hasTasks ? ` 任务勾选:${tasksChecked}/${tasksTotal}` : null,
2032
+ ...(heartbeat || []),
1439
2033
  ` 子步:${subDone.length}/${SUBSTEPS.length}${subDone.length > 0 ? `(${subDone.join('、')})` : ''}`,
1440
2034
  subLeft.length > 0 ? ` 待办:${subLeft.join('、')}` : '',
1441
2035
  st.legacy_fallback ? ` ⚠️ 已升厚(legacy_fallback)——剩余流程走 run <stage>` : '',
@@ -1446,6 +2040,7 @@ export async function cmdFlow(args, cwd, specDir = null) {
1446
2040
  const change = getFlag('--change')
1447
2041
  if (!change) { console.error('❌ flow done 需 --change <名>'); process.exit(2) }
1448
2042
  validateChangeName(change)
2043
+ await reportAgentLog(change, sub)
1449
2044
  // --refreeze(2026-09-25-sentinel-evidence-freeze ⑤):重置 patch 子步标记强制下次重冻结
1450
2045
  // (冻结面归属有误时的人工逃生口——提交面已不过 foreign 切分,dirty 面排除有警告指引到此)
1451
2046
  if (hasFlag('--refreeze')) {
@@ -1456,13 +2051,17 @@ export async function cmdFlow(args, cwd, specDir = null) {
1456
2051
  console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
1457
2052
  }
1458
2053
  }
1459
- return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty') })
2054
+ return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), allowBatchTick: hasFlag('--allow-batch-tick') })
1460
2055
  }
1461
2056
  if (sub === 'amend-draft') {
1462
2057
  // 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;
1463
2058
  // 首版原文 body 永存(切片四 editRatio 基准)。
1464
2059
  const change = getFlag('--change')
1465
2060
  if (!change) { console.error('❌ flow amend-draft 需 --change <名>'); process.exit(2) }
2061
+ // 变更名白名单与 start/status/done 同门(评审 P1 修复:amend-draft 此前无校验,未检字符串
2062
+ // 会经 reportAgentLog 的 context.change_key 进产物与平台上报——与「白名单名无注入面」的设计声明对齐)
2063
+ validateChangeName(change)
2064
+ await reportAgentLog(change, sub)
1466
2065
  const changeDir = join(specBase, 'changes', change)
1467
2066
  const runtimeRoot = resolveRuntimeRoot({}, specBase)
1468
2067
  const { amendFlowDraft } = await import('./flow-draft.js')
@@ -1485,7 +2084,12 @@ export async function cmdFlow(args, cwd, specDir = null) {
1485
2084
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
1486
2085
  return r
1487
2086
  }
1488
- console.error('用法: sillyspec flow start --change <名> --input "<任务>" [--thick|--with-tasks] | sillyspec flow done --change <名>')
2087
+ console.error('用法: sillyspec flow start --change <名> --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json]')
2088
+ console.error(' --input 可照抄形态(引号内换行合法):')
2089
+ console.error(' sillyspec flow start --change <名> --input "<动机与背景>')
2090
+ console.error('')
2091
+ console.error(' 成功标准:')
2092
+ console.error(' - <可验证标准>"')
1489
2093
  process.exit(2)
1490
2094
  }
1491
2095