sillyspec 3.32.10 → 3.33.1

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 (197) 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 +66 -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/archive-delta.js +4 -3
  126. package/src/ceremony-tier.js +2 -1
  127. package/src/change-delete.js +6 -5
  128. package/src/contract-matrix.js +2 -1
  129. package/src/cross-placement.js +5 -1
  130. package/src/db.js +8 -2
  131. package/src/decision-distill.js +29 -3
  132. package/src/decision-redomain.js +148 -0
  133. package/src/deleted-ledger.js +2 -1
  134. package/src/design-facts.js +3 -3
  135. package/src/docs-debt.js +3 -3
  136. package/src/doctor-diagnostics.js +8 -3
  137. package/src/flow-draft.js +2 -1
  138. package/src/flow-parity.js +10 -2
  139. package/src/flow.js +112 -63
  140. package/src/forensics.js +236 -0
  141. package/src/fr-index.js +2 -1
  142. package/src/friction-ledger.js +2 -1
  143. package/src/friction-tally.js +8 -4
  144. package/src/handoff.js +2 -1
  145. package/src/hooks/worktree-guard.js +5 -5
  146. package/src/index.js +75 -8
  147. package/src/knowledge-graph.js +3 -4
  148. package/src/knowledge-hits.js +2 -1
  149. package/src/knowledge-vector.js +2 -1
  150. package/src/module-impact.js +2 -5
  151. package/src/modules.js +31 -0
  152. package/src/progress/consistency-doctor.js +5 -2
  153. package/src/progress/stage-machine.js +6 -2
  154. package/src/progress.js +19 -8
  155. package/src/quicklog.js +194 -9
  156. package/src/review-dispatch.js +4 -3
  157. package/src/review-tier.js +2 -1
  158. package/src/route-hindsight.js +4 -3
  159. package/src/run/bg-sync.js +10 -3
  160. package/src/run/command.js +13 -11
  161. package/src/run/complete-handlers.js +40 -37
  162. package/src/run/complete.js +12 -8
  163. package/src/run/gate-snapshot-ledger.js +2 -1
  164. package/src/run/gate-snapshot.js +57 -11
  165. package/src/run/gates.js +38 -21
  166. package/src/run/lint-ownership.js +37 -0
  167. package/src/run/prompt.js +46 -30
  168. package/src/run/scan-profile.js +2 -1
  169. package/src/run/shared.js +109 -5
  170. package/src/run/stage.js +5 -4
  171. package/src/run/test-ledger.js +2 -1
  172. package/src/run/verify-quality-scan.js +21 -6
  173. package/src/runtime-paths.js +189 -0
  174. package/src/scan-refresh.js +4 -3
  175. package/src/scope-audit.js +7 -6
  176. package/src/sentinel-assertions.js +37 -0
  177. package/src/spec-sync.js +118 -23
  178. package/src/stage-contract.js +24 -2
  179. package/src/stage-review.js +5 -3
  180. package/src/stages/brainstorm.js +72 -21
  181. package/src/stages/plan-postcheck.js +13 -2
  182. package/src/stages/plan.js +19 -0
  183. package/src/sync-noise.js +3 -2
  184. package/src/sync.js +71 -15
  185. package/src/task-done.js +2 -1
  186. package/src/task-review.js +7 -5
  187. package/src/task-tick.js +23 -1
  188. package/src/test-bindings.js +32 -3
  189. package/src/ui-visual.js +119 -4
  190. package/src/validate-artifacts.js +2 -1
  191. package/src/verify-draft.js +2 -1
  192. package/src/verify-postcheck.js +128 -27
  193. package/src/verify-probes.js +157 -52
  194. package/src/watcher.js +15 -9
  195. package/src/worktree-apply.js +28 -3
  196. package/src/write-audit.js +2 -1
  197. package/templates/agents-instruction.md +1 -1
package/src/flow.js CHANGED
@@ -26,12 +26,13 @@
26
26
  * 幂等循 task-done 先例:子步各查自身完成标记,中断半态重入断点续,中段失败精确报告。
27
27
  */
28
28
  import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
29
+ import { runtimeStatePath, runtimeTempPath } from './runtime-paths.js'
29
30
  import { join, relative, dirname } from 'node:path'
30
31
  import { DatabaseSync } from 'node:sqlite'
31
32
  import yaml from 'js-yaml'
32
33
  import { git, gitQuiet } from './git-helper.js'
33
34
  import { writeAtomicSync } from './fs-atomic.js'
34
- import { resolveRuntimeRoot, triggerSync, assertDatedChangeName } from './run/shared.js'
35
+ import { resolveRuntimeRoot, triggerSync, triggerSyncTerminalStrict, assertDatedChangeName } from './run/shared.js'
35
36
  import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
36
37
  import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
37
38
  // 2026-10-13-thin-sync-stage:FLOW_STATE_FILE/readFlowState 单一源上移叶子模块 flow-state.js
@@ -686,18 +687,24 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
686
687
  try {
687
688
  const { spawnWatcher } = await import('./watcher.js')
688
689
  const w = await spawnWatcher(cwd, change, { specBase })
689
- if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
690
+ if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/state/watcher-events-${change}.jsonl(恒带 provisional:true)`)
690
691
  } catch { /* 观测旁路 best-effort */ }
691
- // adopt 标题(用户需求 2026-09-29):头脑风暴 proposal 的 H1 是人写语义标题,
692
- // deriveTitleFromLinkedChange 提取;--title 优先;兜底变更名。adopt 路径此前完全不
693
- // 注册 changes 行——initChange 补插(幂等),既有行 title 空时回填。
692
+ // adopt 标题(2026-09-29;2026-10-16-short-change-title 提炼;2026-10-16-title-range-gate
693
+ // 硬区间 10-30):头脑风暴 proposal H1 人写语义标题(纯模板 H1 回退动机首句)→ --title
694
+ // 优先(合区间校验,超界 exit 2)→ --input 首行提炼 → 兜底变更名。收编是既成事实的
695
+ // 延续面,非 flag 的超界不硬断(收编半态不可恢复)打 ⚠️ 强提示。
694
696
  try {
695
- const { deriveTitleFromLinkedChange, deriveChangeTitle } = await import('./quicklog.js')
696
- const adoptTitle = titleFlag || deriveTitleFromLinkedChange(specBase, change) || deriveChangeTitle(input) || change
697
+ const { deriveTitleFromLinkedChange, deriveShortChangeTitle, isValidChangeTitle, shortTitleRejectMessage, shortTitleRangeWarnLine } = await import('./quicklog.js')
698
+ if (titleFlag && !isValidChangeTitle(titleFlag)) {
699
+ console.error(shortTitleRejectMessage(titleFlag))
700
+ process.exit(2)
701
+ }
702
+ const adoptTitle = titleFlag || deriveTitleFromLinkedChange(specBase, change) || deriveShortChangeTitle(input) || change
697
703
  pm.initChange(cwd, change, { title: adoptTitle })
698
704
  const _arow = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
699
705
  if (_arow && !_arow.title) pm.updateChangeMeta(cwd, change, { title: adoptTitle })
700
706
  console.log(`🏷️ 变更标题:${adoptTitle}(收编自头脑风暴产物;重入 --title 可改)`)
707
+ if (!isValidChangeTitle(adoptTitle)) console.log(shortTitleRangeWarnLine(adoptTitle))
701
708
  } catch { /* adopt 标题 best-effort */ }
702
709
  const materials = materialPaths(specBase, change, changeDir)
703
710
  const artifacts = changeArtifactPaths(changeDir)
@@ -719,7 +726,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
719
726
  `② 执行走任务循环:对 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 四节槽,以其为准)。`,
720
727
  `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
721
728
  `✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
722
- ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;`,
729
+ ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;勾选修改与任务交付同一提交入库(pathspec 带上本变更 tasks.md——task tick 回显给现成命令);`,
723
730
  ` 任务拟法:收口/推送/发布类动作不单列 task(协议自身承担,成功标准照写 requirements);确需单列的,勾选前先有消息预埋 task-NN token 的交付提交(回执类证据写进提交消息)。`,
724
731
  ` flow status --change ${change} 为自愿查看/恢复面(非协议必需,D-007 中间零必需交互;恢复时给下一任务指针与进度)。`,
725
732
  `⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
@@ -780,17 +787,24 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
780
787
  writeFlowState(changeDir, { autopilot: true })
781
788
  console.log(`🤖 已声明 autopilot:spec 断点人审豁免(用户显式声明留痕)`)
782
789
  }
783
- // 变更标题补写(用户需求 2026-09-29:title=中文概括 ≤50 字,建议 ~20 字)——存量在途
784
- // 变更 title 空/缺失时的回填通道:显式 --title 恒生效(重入改标题);否则从 --input 首
785
- // 行推导、仅当库内 title 为空才写(不覆盖已有语义标题)。best-effort 不阻断恢复简报。
790
+ // 变更标题补写(2026-09-29 title=中文概括;2026-10-16-short-change-title 提炼 ≤30;
791
+ // 2026-10-16-title-range-gate 硬区间 10-30)——存量在途变更 title 空/缺失时的回填通道:
792
+ // 显式 --title 恒生效(重入改标题)且必须合区间(超界 exit 2,参数面最易修正);提炼补写
793
+ // 仅当库内 title 为空才写(首写者胜),超界不阻断(resume 不可断)打 ⚠️ 强提示。
786
794
  try {
787
- const { deriveChangeTitle } = await import('./quicklog.js')
795
+ const { deriveShortChangeTitleEx, isValidChangeTitle, shortTitleRejectMessage, shortTitleRangeWarnLine, SHORT_TITLE_HARDCUT_HINT } = await import('./quicklog.js')
796
+ if (titleFlag && !isValidChangeTitle(titleFlag)) {
797
+ console.error(shortTitleRejectMessage(titleFlag))
798
+ process.exit(2)
799
+ }
788
800
  const _row = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
789
801
  const _cur = _row ? (_row.title || '') : ''
790
- const _derived = titleFlag || deriveChangeTitle(input)
791
- if (_derived && (titleFlag || !_cur)) {
792
- pm.updateChangeMeta(cwd, change, { title: _derived })
793
- console.log(`🏷️ 变更标题${titleFlag ? '已更新' : '已补写'}:${_derived}(面板显示用;建议 ~20 字中文概括,重入 --title 可改)`)
802
+ const _derived = titleFlag ? { title: titleFlag, hardCut: false } : deriveShortChangeTitleEx(input)
803
+ if (_derived && _derived.title && (titleFlag || !_cur)) {
804
+ pm.updateChangeMeta(cwd, change, { title: _derived.title })
805
+ console.log(`🏷️ 变更标题${titleFlag ? '已更新' : '已补写'}:${_derived.title}(面板显示用;须 10-30 字、建议 ~20 字,重入 --title 可改)`)
806
+ if (_derived.hardCut) console.log(SHORT_TITLE_HARDCUT_HINT)
807
+ if (!titleFlag && !isValidChangeTitle(_derived.title)) console.log(shortTitleRangeWarnLine(_derived.title))
794
808
  }
795
809
  } catch { /* 标题补写 best-effort */ }
796
810
  // 知识注入(2026-09-25-thin-fr-inject-parity):resume 路径域路由走基线以来文件面——
@@ -893,16 +907,29 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
893
907
  // 形态信号:清晰度门管「需求说不清楚」(预段收编),升厚只留用户决策(--upgrade-thick 同意门)
894
908
  // 与运行时证据(实测失败升档/edit_ratio/评审——风险面在收口时点按承诺词/diff 原语/盲维判定)。
895
909
 
896
- // 变更标题(用户需求 2026-09-29:title=中文概括 ≤50 字、建议 ~20 字,agent 总结):
897
- // --title 显式指定优先;否则 --input 首行推导(agent 在 input 首行写一句中文概括即为标题);
898
- // 两者皆缺用变更名兜底(可辨识但不达意——横幅提示下次带 --title)。
899
- let changeTitle = null
910
+ // 变更标题硬区间门(2026-10-16-title-range-gate:10-30 字必须、建议 ~20、强制 agent 命名;
911
+ // 提炼口径见 2026-10-16-short-change-title):--title 显式 > --input 首行提炼 > 变更名兜底。
912
+ // agent 命名时刻强制:不在区间/无 --input 兜底 → 拒绝启动(exit 2,在任何状态写入前——
913
+ // 零半态);平台 writer 预建空目录(自动派发,无 agent 对话面)豁免硬门仅 ⚠️ 留痕。
914
+ let _st = null
915
+ let _rejectMsg = null
900
916
  try {
901
- const { deriveChangeTitle } = await import('./quicklog.js')
902
- changeTitle = titleFlag || deriveChangeTitle(input) || change
903
- } catch { changeTitle = titleFlag || change }
917
+ const _q = await import('./quicklog.js')
918
+ _st = _q.resolveStartTitle({ titleFlag, input, change, enforce: !proceedFreshEmptyDir })
919
+ if (!_st.ok) _rejectMsg = _q.shortTitleRejectMessage(_st.kind === 'fallback' ? _st.rejectedTitle : _st.title)
920
+ } catch { /* quicklog 不可用(实践不可达):降级保持旧行为兜底 */ _st = { title: titleFlag || change, ok: true, kind: titleFlag ? 'flag' : 'fallback', hardCut: false } }
921
+ if (_rejectMsg) {
922
+ console.error(_rejectMsg)
923
+ process.exit(2)
924
+ }
925
+ const changeTitle = _st.title
904
926
  pm.initChange(cwd, change, { title: changeTitle })
905
- console.log(`🏷️ 变更标题:${changeTitle}${changeTitle === change ? '(变更名兜底——下次带 --title "<≤20 字中文概括>" 更达意)' : '(面板显示用;建议 ~20 字中文概括,重入 flow start --title 可改)'}`)
927
+ console.log(`🏷️ 变更标题:${changeTitle}(面板显示用;须 10-30 字、建议 ~20 字,重入 flow start --title 可改)`)
928
+ try {
929
+ const { isValidChangeTitle, shortTitleRangeWarnLine, SHORT_TITLE_HARDCUT_HINT } = await import('./quicklog.js')
930
+ if (_st.hardCut) console.log(SHORT_TITLE_HARDCUT_HINT)
931
+ if (proceedFreshEmptyDir && !isValidChangeTitle(changeTitle)) console.log(shortTitleRangeWarnLine(changeTitle))
932
+ } catch { /* 提示行 best-effort */ }
906
933
  try {
907
934
  const { resolveSessionIdentity } = await import('./progress.js')
908
935
  const { session } = resolveSessionIdentity({ flagSession: null, cwd })
@@ -915,7 +942,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
915
942
  try {
916
943
  const { spawnWatcher } = await import('./watcher.js')
917
944
  const r = await spawnWatcher(cwd, change, { specBase })
918
- if (r.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
945
+ if (r.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/state/watcher-events-${change}.jsonl(恒带 provisional:true)`)
919
946
  } catch { /* 观测旁路 best-effort,绝不阻断协议面 */ }
920
947
 
921
948
  const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
@@ -1112,13 +1139,13 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1112
1139
  ` 也有 1/4 抽查采样。要强制/豁免可重启时带 --review / --no-review${reviewForce === true ? '(本变更已声明 --review)' : reviewForce === false ? '(本变更已声明 --no-review)' : ''}。`,
1113
1140
  `⚠️ 交付纪律:收口前先把交付代码用显式 pathspec 提交(git add -- <文件> && git commit)——`,
1114
1141
  ` patch 冻结件范围=baseline..HEAD 提交面,未提交的代码不进审计件(R16 评审 P2 实证);`,
1115
- ` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证)。`,
1142
+ ` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证;task tick 回显会给现成命令)。`,
1116
1143
  `✅ 任务面归你(工作分解契约,2026-10-07-thin-tasks-v3):tasks.md 是你的工作队列——机器种子按真实实现路径改写`,
1117
1144
  ` (每行=可独立完成并当场验证的一步:做什么+怎么验证;增删改随意,保持 \`- [ ] task-NN:\` 行形态);`,
1118
1145
  ` 执行循环:对每个未勾行 Working on task N/M → 做一件 → 测试绿后当场勾这一格 → 下一行——CLI 以 checkbox 为进度状态机`,
1119
1146
  ` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
1120
1147
  ` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
1121
- ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
1148
+ ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认;pathspec 带上本变更 tasks.md——勾选修改与任务交付同一提交入库,task tick 回显会给现成命令)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
1122
1149
  // 任务拟法指引(2026-10-12-closeout-task-guidance,三次「哨兵抓收口型任务零证据」复盘):
1123
1150
  // 成功标准≠工作分解——收口/推送/发布是协议自身承担的动作;单列则证据(token 提交)必须先于勾选存在。
1124
1151
  ` 任务拟法:收口/推送/发布类动作不单列 task(提交、归档、推送是 flow start→done 协议自身承担的动作——成功标准照写 requirements,勿逐字搬进任务清单);确需单列的收口/操作型任务,勾选前先做消息预埋 task-NN token 的交付提交——回执类证据(push/publish 回执、构建日志路径)写进提交消息,勿依赖勾选后补;`,
@@ -1138,7 +1165,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1138
1165
  ` ②③断点 agent 自主跳过。`,
1139
1166
  ` 随时可查进度:sillyspec flow status --change ${change}`,
1140
1167
  ...(detectUiTouch(input)
1141
- ? [...buildUiGuidanceLines(), '']
1168
+ ? [...buildUiGuidanceLines(specBase), '']
1142
1169
  : []),
1143
1170
  ``,
1144
1171
  `【协议调用 2/2(干完后)】sillyspec flow done --change ${change}`,
@@ -1177,7 +1204,7 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
1177
1204
  ? (gitQuiet(cwd, ['rev-list', '--count', `${st.baseline_commit}..HEAD`]) || '0').toString().trim()
1178
1205
  : '?'
1179
1206
  const dirty = changedFilesSinceBaseline(cwd, st.baseline_commit).length
1180
- const hasLedger = existsSync(join(runtimeRoot, `verify-quality-scan-${change}.json`))
1207
+ const hasLedger = existsSync(runtimeStatePath(runtimeRoot, `verify-quality-scan-${change}.json`))
1181
1208
  console.log([
1182
1209
  `🔁 flow start 恢复简报(重入): ${change}(tier=${st.tier}${st.legacy_fallback ? ',legacy_fallback=true 混跑回退中' : ''})`,
1183
1210
  `══════════════════════════════════════════════════════`,
@@ -1265,7 +1292,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1265
1292
  // 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
1266
1293
  const appendTelemetry = (review) => {
1267
1294
  try {
1268
- appendFileSync(join(runtimeRoot, 'flow-telemetry.jsonl'), JSON.stringify({
1295
+ appendFileSync(runtimeTempPath(runtimeRoot, 'flow-telemetry.jsonl'), JSON.stringify({
1269
1296
  ts: new Date().toISOString(), change, protocolCalls: 2,
1270
1297
  draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
1271
1298
  routeHint: st.route_hint ?? null, tier: st.tier, upgraded: st.upgrade_reason ?? null,
@@ -1369,6 +1396,22 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1369
1396
  let _sentinelNonMirrorTasks = null // 镜像豁免面外的任务数(null=哨兵未跑;0=纯镜像任务面——节奏 advisory 静默)
1370
1397
  let _sentinelEvidenceComplete = false // 哨兵证据完备(status=complete——每格已勾任务均有 token/review.json):单拍降级判据(2026-10-13-batchtick-evidence-domain-hint)
1371
1398
  let _autopilotTicked = 0 // 机器代勾格数(>0 时勾选节奏门降 advisory——代勾是单拍多格机械写,非 agent 纪律面)
1399
+ // 勾选观测共享采集(2026-10-17-tick-timing-source):watcher 事件流 + task-ticks.jsonl 侧车
1400
+ // 一次读取——时点判定与勾选节奏门同源消费(此前节奏门单独读,现两门一份数据;检测逻辑
1401
+ // 与优先序不变:事件流先判、空结果回退侧车突发)。侧车=CLI tick 逐格毫秒时戳事实源(随
1402
+ // 归档进 git、不受观测盲窗影响)。任一源缺失按空(fail-open),绝不阻断收口。
1403
+ const _tickObs = await (async () => {
1404
+ try {
1405
+ const { readWatcherEvents } = await import('./watcher.js')
1406
+ const { detectBatchCheckCadence, detectTickBurst } = await import('./sentinel-assertions.js')
1407
+ const stream = readWatcherEvents({ runtimeRoot, change })
1408
+ let ticks = []
1409
+ try { ticks = (await import('./timeline.js')).parseTickLog(readFileSync(join(changeDir, 'task-ticks.jsonl'), 'utf8')) } catch { /* 侧车缺失/不可读按空(fail-open) */ }
1410
+ let batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
1411
+ if (!batch) batch = detectTickBurst(ticks)
1412
+ return { stream, ticks, batch }
1413
+ } catch { return { stream: { exists: false, events: [] }, ticks: [], batch: null } }
1414
+ })()
1372
1415
  // 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
1373
1416
  // token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
1374
1417
  // 提交计数不因多行正文失真。
@@ -1436,20 +1479,31 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1436
1479
  if (sent.status === 'complete') {
1437
1480
  _sentinelEvidenceComplete = true
1438
1481
  console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
1439
- // 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
1440
- // == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
1441
- // 这是提醒边干边勾纪律,非假勾)。tasks.md 全程 untracked(未随交付提交)时同款提醒。
1442
- // 首提锚定(2026-10-07-tick-timing-first-commit):--reverse 全 history 首行=首次提交——
1443
- // 旧实现 -n 1 取最近一次提交,每任务随交付提交 tasks.md 的渐进形态(首提早于 HEAD)
1444
- // 被误报一把勾(完整实测实证:历史 0→1→2 渐进勾选仍出警告)。
1482
+ // 勾选时点判定(2026-10-17-tick-timing-source 换源):判据从「tasks.md git 首次提交
1483
+ // == 收口最后提交」整体退役——git 历史形态被官方允许的 reset --soft 压扁无声摧毁
1484
+ // (tick-timing-squash-blindspot 坑:并行会话压扁归档把边干边勾的渐进历史抹平,警告
1485
+ // 照出,结构性误报;且 tasks.md 不进交付 pathspec 的纪律下「untracked 直至归档」恒
1486
+ // 复现)。换源=task-ticks.jsonl 侧车墙钟时戳(detectTickTiming 纯函数:跨度超 120s
1487
+ // =边干边勾确认;挤 ≤120s 短窗且变更存活 ≥10×跨度=末尾集中勾选)——墙钟时戳是文件
1488
+ // 事实,压扁/rebase 不可改写,与「审计真相锚 change.patch sha256、不依赖 git 形态」
1489
+ // 口径对齐。节奏门已检出单拍跳/突发时让位(防双重警告——其判定覆盖同一批格子);
1490
+ // 无侧车观测输出一行 task tick 教学(不再作结构性警告);advisory 不阻断语义不变。
1445
1491
  try {
1446
- const _firstRaw = gitQuiet(cwd, ['log', '--reverse', '--format=%h', '--', tasksPath])
1447
- const tasksIn = _firstRaw ? (String(_firstRaw).split(/\r?\n/).map((x) => x.trim()).filter(Boolean)[0] || null) : null
1448
- const lastIn = gitQuiet(cwd, ['log', '--format=%h', '-n', '1'])
1449
- if (!tasksIn) {
1450
- console.warn(`⚠️ 勾选时点:tasks.md 未随交付提交(untracked 直至归档)——勾选证据链靠归档兜底,git 历史不可回溯;下次收口前 tasks.md 一并显式 pathspec 提交`)
1451
- } else if (String(tasksIn).trim() === String(lastIn).trim()) {
1452
- console.warn(`⚠️ 勾选时点:tasks.md 的首次 git 提交 == 收口最后提交(一把勾模式)——token 证据齐放行,但边干边勾是进度锚(OS Guardrails 同款纪律),下次完成单元即勾`)
1492
+ if (_tickObs.batch) {
1493
+ /* 节奏门将报单拍多格/突发——时点判定让位 */
1494
+ } else {
1495
+ const _baseRaw = st && st.baseline_commit ? gitQuiet(cwd, ['show', '-s', '--format=%ct', String(st.baseline_commit)]) : null
1496
+ const _baseTs = Number(String(_baseRaw ?? '').trim())
1497
+ const { detectTickTiming } = await import('./sentinel-assertions.js')
1498
+ const t = detectTickTiming({ ticks: _tickObs.ticks, lifespanMs: Number.isFinite(_baseTs) && _baseTs > 0 ? Date.now() - _baseTs * 1000 : NaN })
1499
+ if (t.status === 'concentrated') {
1500
+ console.warn(`⚠️ 勾选时点:${t.count} 格勾选时戳集中在 ${Math.max(1, Math.round(t.spanMs / 1000))}s 短窗内(末尾集中勾选形态)——token 证据齐放行,但边干边勾是进度锚,下次完成单元即勾(task tick 回显含现成提交命令)`)
1501
+ } else if (t.status === 'spread') {
1502
+ console.log(`✅ 勾选时点:${t.count} 格勾选时戳跨度 ${Math.max(1, Math.round(t.spanMs / 60000))} 分钟——边干边勾确认(判据=task-ticks 侧车墙钟时戳,不受 git 压扁影响)`)
1503
+ } else if (t.status === 'no-ticks') {
1504
+ console.log(`ℹ️ 勾选时点:无 tick 时戳观测(未走 task tick)——下次勾选建议 sillyspec task tick --change ${change} --task task-NN(即时回显进度与现成提交命令)`)
1505
+ }
1506
+ /* 'single'/'no-anchor' 静默:单格无节奏可言;缺存活锚 fail-open 不判 */
1453
1507
  }
1454
1508
  } catch { /* 时点判定 fail-soft */ }
1455
1509
  }
@@ -1459,7 +1513,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1459
1513
  // 任务面与实际不符可先改写 tasks.md(工作分解归 agent)再逐格勾。token 代勾(governance-
1460
1514
  // autopilot)已在上方先跑:有证据未勾的格子已代勾,此处拒的纯属零证据未完成面。
1461
1515
  if (sent.claimTotal > 0 && sent.checked < sent.claimTotal) {
1462
- console.error(`🚫 任务未全勾(${sent.checked}/${sent.claimTotal})——tasks.md 是完成状态机:完成一件勾一格(每格勾选需对应提交带 task-NN token 或 review.json),全勾后重跑 flow done`)
1516
+ console.error(`🚫 任务未全勾(${sent.checked}/${sent.claimTotal})——tasks.md 是完成状态机:完成一件勾一格(每格勾选需对应提交带 task-NN token 或 review.json;提交 pathspec 带上本变更 tasks.md——勾选修改与任务交付同一提交入库),全勾后重跑 flow done`)
1463
1517
  console.error(' 任务面与实际实现路径不符时,先改写 tasks.md(工作分解归你,增删改随意)再逐格勾;查下一任务:sillyspec task tick --change ' + `${change}` + ' --task task-NN(回显进度与指针)')
1464
1518
  appendTelemetry({ sentinel: 'incomplete-tasks', checked: sent.checked, total: sent.claimTotal })
1465
1519
  reportMidFail('ledger')
@@ -1477,19 +1531,12 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1477
1531
  // 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):CLI 收口端快连点对事件流不可见(单格
1478
1532
  // 精确跳),空结果时回退 task-ticks.jsonl 毫秒时戳判突发——CLI 与直接 Edit 两条路径同判。
1479
1533
  try {
1480
- const { readWatcherEvents } = await import('./watcher.js')
1481
- const { detectBatchCheckCadence, detectTickBurst, resolveBatchTickAction } = await import('./sentinel-assertions.js')
1482
- const stream = readWatcherEvents({ runtimeRoot, change })
1483
- let batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
1484
- if (!batch) {
1485
- // 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):事件流空结果时读 change 目录
1486
- // task-ticks.jsonl(CLI tick 毫秒时戳事实源,随归档进 git、不受观测盲窗影响)判快连点;
1487
- // 事件流结果优先不双重报告;侧车缺失/坏文件按无证据静默(fail-open)。
1488
- try {
1489
- const { parseTickLog } = await import('./timeline.js')
1490
- batch = detectTickBurst(parseTickLog(readFileSync(join(changeDir, 'task-ticks.jsonl'), 'utf8')))
1491
- } catch { /* 侧车缺失/不可读 → 无侧车证据(fail-open) */ }
1492
- }
1534
+ const { resolveBatchTickAction } = await import('./sentinel-assertions.js')
1535
+ // 观测消费共享采集(2026-10-17-tick-timing-source):事件流/侧车/突发判定已在前文哨兵段
1536
+ // 前置采集为 _tickObs(时点判定与节奏门同源一份数据;读取仅前移,检测逻辑与优先序不变
1537
+ // ——事件流先判、空结果回退侧车突发,侧车缺失按空 fail-open)。
1538
+ const stream = _tickObs.stream
1539
+ const batch = _tickObs.batch
1493
1540
  if (batch) {
1494
1541
  // 代勾计数取「本拍实时代勾 ∨ 持久化代勾」较大者(二轮 P2-B:重入漂移防护);决策纯函数
1495
1542
  // 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
@@ -2449,7 +2496,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2449
2496
  // ⑥ events:事件收口(watcher 观测旁路 best-effort 读回,非真相源)
2450
2497
  if (st.substeps?.events === 'done') { skip('events') } else {
2451
2498
  try {
2452
- const eventsPath = join(runtimeRoot, `watcher-events-${change}.jsonl`)
2499
+ const eventsPath = runtimeStatePath(runtimeRoot, `watcher-events-${change}.jsonl`)
2453
2500
  if (existsSync(eventsPath)) {
2454
2501
  const n = readFileSync(eventsPath, 'utf8').split('\n').filter(Boolean).length
2455
2502
  console.log(`📊 watcher 事件收口:${n} 条 provisional 事件在案(.runtime 留档,平台展示面)`)
@@ -2483,8 +2530,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2483
2530
  console.log(` 注:knowledge「最近确认」hash 为溯源快照,压扁后成孤儿引用属预期(非活引用)。`)
2484
2531
  }
2485
2532
  } catch { /* 指引 best-effort */ }
2486
- // 平台同步(同 flow start 尾部接线——归档后的 docs/knowledge/FR 面随本轮回推平台)
2487
- try { await triggerSync(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
2533
+ // 平台同步(同 flow start 尾部接线——归档后的 docs/knowledge/FR 面随本轮回推平台)。
2534
+ // 终态强校验(2026-10-17-archive-half-state-location-stuck-active):flow done 到此 =
2535
+ // 变更已归档,文件树推送失败不再静默半态(🚨 告警 + 自救命令 + exit 1)
2536
+ try { await triggerSyncTerminalStrict(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
2488
2537
  return { change, substeps: doneList }
2489
2538
  }
2490
2539
 
@@ -2663,7 +2712,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2663
2712
  console.error(`❌ ${e.message}`)
2664
2713
  // 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
2665
2714
  console.error(' 重试(--input 引号内换行合法,可照抄):')
2666
- console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2715
+ console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
2667
2716
  console.error('')
2668
2717
  console.error(' 成功标准:')
2669
2718
  console.error(' - <可验证标准>"')
@@ -2892,9 +2941,9 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2892
2941
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
2893
2942
  return r
2894
2943
  }
2895
- console.error('用法: sillyspec flow start --change <名> --input "<一句中文概括(首行即变更标题)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
2944
+ console.error('用法: sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
2896
2945
  console.error(' --input 可照抄形态(引号内换行合法):')
2897
- console.error(' sillyspec flow start --change <名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2946
+ console.error(' sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
2898
2947
  console.error('')
2899
2948
  console.error(' 成功标准:')
2900
2949
  console.error(' - <可验证标准>"')
@@ -0,0 +1,236 @@
1
+ /**
2
+ * forensics.js — 会话取证证据包装配 + 取证任务书渲染(2026-10-15-session-forensics)
3
+ *
4
+ * 定位:`sillyspec diagnose --change <名>` 的只读装配面。对照 obra/superpowers
5
+ * diagnosing-superpowers 的取证形态做 SillySpec 原生版——分工沿本仓哲学:
6
+ * CLI 装配机械事实(证据包六面),agent 做语义判断(按任务书读 transcript、出
7
+ * path:line 证据报告)。全程零写入:证据源全是事后读取的静态文件,单源缺失/
8
+ * 损坏 fail-soft 空清单+注记,不抛错不阻断(取证要容忍现场变动)。
9
+ *
10
+ * 数据源(全部既有,零新格式):
11
+ * - 变更目录 / 归档目录(flow-state.yaml 身份面、review 轮次文件)
12
+ * - <runtimeRoot>/agent-session-log.json(run 命令登记的会话日志路径,
13
+ * 按 change_key 精确相等过滤——登记面唯一键,无子串误命中面)
14
+ * - <runtimeRoot>/friction-ledger.json(readFrictionLedger 既有 API)
15
+ * - <runtimeRoot>/verify-runs/<时间戳目录>/test-result.json(mtime 近序取前 N)
16
+ *
17
+ * 红线:零内容枚举——任务书六维度是结构骨架(CLI 自有任务书面集合,同
18
+ * review_probes scope 判例),不是内容词表;CLI 不解析 transcript 本体
19
+ * (三 harness 格式 × 演化泥潭,解读交给 agent)。
20
+ */
21
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+ import yaml from 'js-yaml'
24
+ import { readFrictionLedger } from './friction-ledger.js'
25
+
26
+ /** verify-runs 证据默认截断数(近序 N 条,防大仓 runtime 灌爆输出;模块内常量——导出即死码债) */
27
+ const VERIFY_RUNS_LIMIT = 3
28
+
29
+ /** 读 JSON 文件,失败/不存在返回 null(fail-soft 取证立场)。 */
30
+ function readJsonSafe(path) {
31
+ try {
32
+ return JSON.parse(readFileSync(path, 'utf8'))
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ /** 读 YAML 文件,失败/不存在返回 null。 */
39
+ function readYamlSafe(path) {
40
+ try {
41
+ return yaml.load(readFileSync(path, 'utf8'))
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 装配证据包六面。纯读函数,零写入。
49
+ * @param {{ specBase: string, runtimeRoot: string, changeName: string, verifyRunsLimit?: number }} p
50
+ * @returns {{ identity: object, agentLogs: object[], friction: object[], verifyRuns: object[], reviews: object[], warnings: string[] }}
51
+ */
52
+ export function assembleForensicsPack({ specBase, runtimeRoot, changeName, verifyRunsLimit = VERIFY_RUNS_LIMIT }) {
53
+ const warnings = []
54
+ const changeDir = join(specBase, 'changes', changeName)
55
+ const archivedDir = join(specBase, 'changes', 'archive', changeName)
56
+
57
+ // ① 变更身份(active / archived / missing + flow-state 态)
58
+ const identity = { changeName, status: 'missing', changeDir: null, archivedDir: null, tier: null, substeps: null }
59
+ if (existsSync(changeDir)) {
60
+ identity.status = 'active'
61
+ identity.changeDir = changeDir
62
+ } else if (existsSync(archivedDir)) {
63
+ identity.status = 'archived'
64
+ identity.changeDir = archivedDir
65
+ identity.archivedDir = archivedDir
66
+ warnings.push('变更已归档——证据包从归档目录读取,transcript 仍按登记路径取')
67
+ }
68
+ const flowStatePath = identity.changeDir ? join(identity.changeDir, 'flow-state.yaml') : null
69
+ if (flowStatePath && existsSync(flowStatePath)) {
70
+ const fs_ = readYamlSafe(flowStatePath)
71
+ if (fs_ && typeof fs_ === 'object') {
72
+ identity.tier = fs_.tier ?? null
73
+ identity.substeps = fs_.substeps && typeof fs_.substeps === 'object' ? fs_.substeps : null
74
+ } else {
75
+ warnings.push(`flow-state.yaml 解析失败(${flowStatePath})——身份面降级`)
76
+ }
77
+ } else {
78
+ warnings.push('无 flow 态(archive 件或无 flow-state.yaml)')
79
+ }
80
+
81
+ // ② agent 会话日志(change_key 精确相等——登记面唯一键)
82
+ const registryPath = join(runtimeRoot, 'agent-session-log.json')
83
+ const registry = readJsonSafe(registryPath)
84
+ let agentLogs = []
85
+ if (registry && Array.isArray(registry.entries)) {
86
+ agentLogs = registry.entries.filter((e) => e && typeof e === 'object' && e.change_key === changeName)
87
+ } else {
88
+ warnings.push(`会话日志登记缺失或不可解析(${registryPath})——本机未跑过 run 命令或登记面损坏`)
89
+ }
90
+
91
+ // ③ 摩擦账本(readFrictionLedger 容错:缺文件/坏 JSON → [])
92
+ const friction = readFrictionLedger(runtimeRoot).filter((e) => e.change === changeName)
93
+
94
+ // ④ verify-runs(mtime 近序,同 mtime 路径字典序破平;截断前 N)
95
+ // fail-soft:runsRoot 为普通文件/权限损坏形态时 readdirSync 抛 ENOTDIR——捕获降级,不阻断取证。
96
+ const runsRoot = join(runtimeRoot, 'verify-runs')
97
+ const verifyRuns = []
98
+ if (existsSync(runsRoot)) {
99
+ let runEntries = []
100
+ try {
101
+ runEntries = readdirSync(runsRoot, { withFileTypes: true })
102
+ } catch {
103
+ warnings.push(`verify-runs 不可读(非目录或损坏形态,${runsRoot})——降级为空`)
104
+ }
105
+ const dirs = runEntries
106
+ .filter((d) => d.isDirectory())
107
+ .map((d) => join(runsRoot, d.name))
108
+ const withTime = dirs
109
+ .map((dir) => {
110
+ try {
111
+ return { dir, mtimeMs: statSync(dir).mtimeMs }
112
+ } catch {
113
+ return null
114
+ }
115
+ })
116
+ .filter(Boolean)
117
+ .sort((a, b) => (b.mtimeMs - a.mtimeMs) || (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0))
118
+ for (const { dir, mtimeMs } of withTime.slice(0, Math.max(1, verifyRunsLimit))) {
119
+ const resultPath = join(dir, 'test-result.json')
120
+ verifyRuns.push({
121
+ dir,
122
+ resultPath: existsSync(resultPath) ? resultPath : null,
123
+ mtimeIso: new Date(mtimeMs).toISOString(),
124
+ })
125
+ }
126
+ } else {
127
+ warnings.push(`无 verify-runs 目录(${runsRoot})——本变更未跑过收口实测或 runtime 已清理`)
128
+ }
129
+
130
+ // ⑤ 评审轮次(review.json + superseded-*,mtime 近序;目录损坏形态同上降级)
131
+ const reviews = []
132
+ if (identity.changeDir && existsSync(identity.changeDir)) {
133
+ let reviewFiles = []
134
+ try {
135
+ reviewFiles = readdirSync(identity.changeDir).filter((f) => /^review\.json(\.superseded-.+)?$/.test(f))
136
+ } catch {
137
+ warnings.push(`变更目录不可读(${identity.changeDir})——评审轮次面降级为空`)
138
+ }
139
+ for (const f of reviewFiles) {
140
+ const p = join(identity.changeDir, f)
141
+ let mtimeIso = null
142
+ try {
143
+ mtimeIso = new Date(statSync(p).mtimeMs).toISOString()
144
+ } catch { /* 文件刚被删——列名不留 mtime */ }
145
+ reviews.push({ file: p, superseded: f !== 'review.json', mtimeIso })
146
+ }
147
+ reviews.sort((a, b) => (b.mtimeIso || '').localeCompare(a.mtimeIso || ''))
148
+ } else {
149
+ warnings.push('变更目录不存在——评审轮次面为空')
150
+ }
151
+
152
+ return { identity, agentLogs, friction, verifyRuns, reviews, warnings }
153
+ }
154
+
155
+ /**
156
+ * 渲染取证任务书(markdown)。六维度是结构骨架不是封闭清单;四纪律钉死
157
+ * diagnosing-superpowers 的核心铁律(path:line 锚、唯一可写产物、先读本体、时间盒)。
158
+ * @param {{ identity: object, agentLogs: object[], friction: object[], verifyRuns: object[], reviews: object[], warnings: string[] }} pack
159
+ * @returns {string}
160
+ */
161
+ export function renderForensicsTaskbook(pack) {
162
+ const { identity, agentLogs, friction, verifyRuns, reviews, warnings } = pack
163
+ const lines = []
164
+ lines.push(`# 会话取证任务书 — ${identity.changeName}`)
165
+ lines.push('')
166
+ lines.push(`【角色】你是取证分析员(干净上下文)——还原这个会话里实际发生了什么,拿证据说话,不替流程辩护也不替人辩护。报告读者是「要决定下一步怎么办的人」。`)
167
+ lines.push('')
168
+ lines.push('## 证据包(CLI 装配的机械事实——先读这些文件本体,再下结论)')
169
+ lines.push('')
170
+ lines.push(`- 变更身份:status=${identity.status}|tier=${identity.tier ?? '无'}|目录=${identity.changeDir ?? '(不存在)'}`)
171
+ lines.push(`- agent 会话日志(${agentLogs.length} 条,change_key=${identity.changeName} 精确过滤):`)
172
+ for (const e of agentLogs) {
173
+ lines.push(` - [${e.harness}/${e.format ?? '?'}] ${e.log_path}(session=${e.session_id ?? '?'}${e.exists === false ? ',⚠️ 文件不在场' : ''})`)
174
+ }
175
+ if (agentLogs.length === 0) lines.push(' - (零命中——本机未登记过本变更的会话日志)')
176
+ lines.push(`- 摩擦账本(${friction.length} 条):${friction.map((e) => JSON.stringify(e.counts ?? {})).join(' / ') || '(无)'}`)
177
+ lines.push(`- verify-runs(近 ${verifyRuns.length} 次):`)
178
+ for (const r of verifyRuns) lines.push(` - ${r.resultPath ?? r.dir + '(无 test-result.json)'} @ ${r.mtimeIso}`)
179
+ lines.push(`- 评审轮次(${reviews.length} 份,含 superseded):`)
180
+ for (const r of reviews) lines.push(` - ${r.file}${r.superseded ? '(superseded——前轮评审结论,对比两轮可还原修复轨迹)' : ''}`)
181
+ if (warnings.length) {
182
+ lines.push(`- 降级注记:${warnings.join(';')}`)
183
+ }
184
+ lines.push('')
185
+ lines.push('## 分析维度(结构骨架——常见失事面,不是封闭清单:证据指向维度外时照报,报告允许增设章节)')
186
+ lines.push('')
187
+ lines.push('1. **时间线重建**:这个会话按时间实际做了什么(步骤推进/等待/回滚/中断点)。')
188
+ lines.push('2. **计划遵从**:做的是计划内的事吗?偏离处有没有留痕(决策/账本/提交消息)?')
189
+ lines.push('3. **重复工作**:有没有同一件事做两遍(重派/重审/重跑)?根因是什么?')
190
+ lines.push('4. **停等与重试**:哪里停等了用户/子代理?重试了几次?是否有可避免的空转?')
191
+ lines.push('5. **成本与轮次**:轮次/token 花在哪了(读文件长尾?全量重验?)?')
192
+ lines.push('6. **门禁摩擦**:gate 回滚/评审打回/实测失败的完整链条与最终清偿方式。')
193
+ lines.push('')
194
+ lines.push('## 取证纪律')
195
+ lines.push('')
196
+ lines.push('1. **每条 finding 必须带 `path:line` 锚,无锚不成立**——锚指向证据包列出的文件或其内容(行号取自你实际读到的文件)。')
197
+ lines.push('2. **唯一可写产物是 `diagnosis-report.md`**(写在变更目录)——禁止修改任何其他文件、禁止 git 操作。固定章节:`## 问题陈述` / `## 时间线` / `## Findings`(分维度)/ `## 无法证实面`(证据不足的疑点如实列,不脑补)。')
198
+ lines.push('3. **先读证据包列出的文件本体再下结论,禁止全量扫读仓**——矛盾经常藏在交叉处,定向读那一处。')
199
+ lines.push('4. **时间盒收敛**:结论存疑时定向补证;连续扩面仍零结论 = 基于已读材料收敛,把疑点写进「无法证实面」。')
200
+ lines.push('')
201
+ lines.push('【无子代理通道时】主会话按本任务书自行分析即可,如实标注执行方式;有子代理通道时按维度分派并行分析、汇总时逐条核对锚点。')
202
+ lines.push('')
203
+ return lines.join('\n')
204
+ }
205
+
206
+ /**
207
+ * 命令入口:sillyspec diagnose --change <名> [--detect] [--json]
208
+ * 只读零写入;--detect 先透传 agent-session-log 现场探测(不落盘语义随既有 cmdAgentLog)。
209
+ * @param {string[]} args - filteredArgs.slice(1)('diagnose' 之后)
210
+ * @param {{ json?: boolean, cwd?: string, specDir?: string|null }} opts
211
+ */
212
+ export async function cmdForensics(args, { json = false, cwd = process.cwd(), specDir = null } = {}) {
213
+ const changeIdx = args.indexOf('--change')
214
+ const changeName = changeIdx >= 0 && args[changeIdx + 1] && !String(args[changeIdx + 1]).startsWith('--') ? args[changeIdx + 1] : null
215
+ if (!changeName) {
216
+ console.error('用法: sillyspec diagnose --change <名> [--detect] [--json] [--spec-dir <路径>]')
217
+ console.error(' 只读装配会话取证证据包 + 取证任务书(path:line 铁律;唯一可写产物是变更目录的 diagnosis-report.md,由执行任务书的 agent 产出)')
218
+ process.exit(2)
219
+ }
220
+ const specBase = specDir || join(cwd, '.sillyspec')
221
+ const runtimeRoot = join(specBase, '.runtime')
222
+
223
+ if (args.includes('--detect')) {
224
+ const { cmdAgentLog } = await import('./agent-session-log.js')
225
+ await cmdAgentLog(['--detect'], { json: false, cwd, specDir })
226
+ }
227
+
228
+ const pack = assembleForensicsPack({ specBase, runtimeRoot, changeName })
229
+ const taskbook = renderForensicsTaskbook(pack)
230
+
231
+ if (json || args.includes('--json')) {
232
+ console.log(JSON.stringify({ ...pack, taskbook }))
233
+ return
234
+ }
235
+ console.log(taskbook.trimEnd())
236
+ }
package/src/fr-index.js CHANGED
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { readChangeTrace, upsertFrBindings, applySupersededToEntryLines, testAnchorFile, resolveTestFileRel } from './test-bindings.js'
21
+ import { modulePathMatches } from './modules.js'
21
22
  import { suggestDomainFromFiles, snapDomainToDictionary } from './knowledge-digest.js'
22
23
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
23
24
  import { basename, dirname, join } from 'path';
@@ -250,7 +251,7 @@ export function matchedModuleIds(moduleIndex, filePath) {
250
251
  const hits = [];
251
252
  for (const [modId, mod] of Object.entries(moduleIndex)) {
252
253
  const paths = (mod && Array.isArray(mod.paths)) ? mod.paths : [];
253
- if (paths.some((pp) => filePath === pp || filePath.startsWith(pp.endsWith('/') ? pp : pp + '/'))) {
254
+ if (paths.some((pp) => modulePathMatches(filePath, pp))) {
254
255
  hits.push(modId);
255
256
  }
256
257
  }
@@ -31,6 +31,7 @@
31
31
  * 本模块自身不打印任何输出。
32
32
  */
33
33
  import { readFileSync, mkdirSync } from 'fs'
34
+ import { runtimeStatePath } from './runtime-paths.js'
34
35
  import { dirname, join } from 'path'
35
36
  import { writeAtomicSync } from './fs-atomic.js'
36
37
  import { withFileLock } from './quicklog.js'
@@ -45,7 +46,7 @@ const LEDGER_CAP = 200
45
46
  const LEDGER_FILENAME = 'friction-ledger.json'
46
47
 
47
48
  function ledgerFilePath(runtimeRoot) {
48
- return join(runtimeRoot, LEDGER_FILENAME)
49
+ return runtimeStatePath(runtimeRoot, LEDGER_FILENAME)
49
50
  }
50
51
 
51
52
  /**