sillyspec 3.27.1 → 3.27.5

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 (224) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/README.md +1 -1
  147. package/eval/README.md +86 -0
  148. package/eval/eval.config.json +28 -0
  149. package/eval/lib/adapters/cli.mjs +63 -0
  150. package/eval/lib/adapters/dryrun.mjs +27 -0
  151. package/eval/lib/config.mjs +82 -0
  152. package/eval/lib/db.mjs +76 -0
  153. package/eval/lib/report.mjs +120 -0
  154. package/eval/lib/runner.mjs +157 -0
  155. package/eval/lib/tasks.mjs +73 -0
  156. package/eval/lib/util.mjs +107 -0
  157. package/eval/lib/workdir.mjs +30 -0
  158. package/eval/run.mjs +83 -0
  159. package/eval/tasks/001-slugify/files/package.json +6 -0
  160. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  161. package/eval/tasks/001-slugify/task.json +6 -0
  162. package/eval/tasks/001-slugify/verify.mjs +28 -0
  163. package/eval/tasks/002-each-day/files/package.json +6 -0
  164. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  165. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  166. package/eval/tasks/002-each-day/task.json +6 -0
  167. package/eval/tasks/002-each-day/verify.mjs +31 -0
  168. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  169. package/eval/tasks/003-word-report/files/package.json +6 -0
  170. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  171. package/eval/tasks/003-word-report/task.json +6 -0
  172. package/eval/tasks/003-word-report/verify.mjs +38 -0
  173. package/package.json +4 -2
  174. package/src/agent-session-log.js +803 -0
  175. package/src/change-list.js +22 -2
  176. package/src/change-risk-profile.js +83 -14
  177. package/src/config-schema.js +18 -3
  178. package/src/contract-matrix.js +153 -8
  179. package/src/datetime.js +20 -0
  180. package/src/decision-distill.js +517 -0
  181. package/src/dispatch/backends/local-agent.js +4 -4
  182. package/src/docs-check.js +227 -0
  183. package/src/docs-debt.js +187 -24
  184. package/src/doctor-diagnostics.js +102 -2
  185. package/src/foreign-declared.js +78 -0
  186. package/src/index.js +99 -10
  187. package/src/init.js +9 -7
  188. package/src/knowledge-match.js +107 -6
  189. package/src/local-register.js +7 -1
  190. package/src/module-resolve.js +12 -1
  191. package/src/modules.js +2 -1
  192. package/src/plan-adopt-waves.js +126 -0
  193. package/src/quicklog.js +61 -7
  194. package/src/run/command.js +139 -20
  195. package/src/run/complete-handlers.js +48 -19
  196. package/src/run/complete.js +43 -3
  197. package/src/run/gates.js +87 -22
  198. package/src/run/prompt.js +84 -42
  199. package/src/run/quick-audit.js +28 -0
  200. package/src/run/shared.js +176 -11
  201. package/src/run/stage.js +23 -0
  202. package/src/run.js +2 -2
  203. package/src/scan-postcheck.js +30 -17
  204. package/src/stage-contract-spec.js +2 -1
  205. package/src/stage-contract.js +68 -4
  206. package/src/stages/archive.js +53 -12
  207. package/src/stages/brainstorm-auto.js +7 -0
  208. package/src/stages/brainstorm.js +11 -0
  209. package/src/stages/doctor.js +157 -1
  210. package/src/stages/execute.js +33 -6
  211. package/src/stages/plan-postcheck.js +79 -18
  212. package/src/stages/plan.js +4 -0
  213. package/src/stages/quick.js +6 -1
  214. package/src/stages/verify.js +31 -14
  215. package/src/sync.js +94 -2
  216. package/src/task-review.js +2 -2
  217. package/src/taskcard-placeholders.js +21 -0
  218. package/src/taskcard.js +10 -1
  219. package/src/verify-postcheck.js +315 -13
  220. package/src/verify-probes.js +46 -4
  221. package/src/worktree-apply.js +184 -7
  222. package/src/worktree-deps.js +69 -0
  223. package/src/worktree.js +201 -58
  224. package/templates/prompts/taskcard-rules.md +1 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 他者会话显式声明文件集(坑 verify-reconcile-foreign-wip,2026-08-23 实证:verify 实测
3
+ * 对账在主仓共享工作区取 git diff,无 meta 回退时把并行会话在途 WIP 全量算进本变更——误
4
+ * 命中他者模块跑他者测试 / 他者删除进本变更删除对账误报)。
5
+ *
6
+ * 归属口径与坑 35(quick 审计豁免,run/shared 的 collectOtherQuickSessionDeclarations)
7
+ * 同款:只认**显式声明**——其他 quick 会话 guard.allowedFiles + 其他变更 design §6 清单;
8
+ * 无主文件保留参与判定(fail-closed,不放松「未声明删除/越界」的抓取底线)。
9
+ *
10
+ * 独立零环模块:被 verify-postcheck / verify-probes / contract-matrix 共用(verify-
11
+ * postcheck import contract-matrix,parity 侧不能反向 import verify-postcheck)。
12
+ */
13
+ import { existsSync, readFileSync, readdirSync } from 'fs'
14
+ import { join } from 'path'
15
+ import { parseFileChangeListDetailed } from './change-list.js'
16
+
17
+ /**
18
+ * @param {string} cwd 项目根
19
+ * @param {string|null} currentChangeName 本变更名(quick 会话名或 change 名,排除自身)
20
+ * @returns {Map<string, string[]>} file → 声明者列表(quick-<hex> 或变更名)
21
+ */
22
+ export function collectForeignDeclaredFiles(cwd, currentChangeName) {
23
+ const foreign = new Map()
24
+ const add = (file, owner) => {
25
+ const f = String(file || '').replace(/\\/g, '/')
26
+ if (!f) return
27
+ if (!foreign.has(f)) foreign.set(f, [])
28
+ if (!foreign.get(f).includes(owner)) foreign.get(f).push(owner)
29
+ }
30
+ try {
31
+ const specBase = join(cwd, '.sillyspec')
32
+ // ① 其他 quick 会话的 --files 显式声明(guard.json)
33
+ const sessionsDir = join(specBase, '.runtime', 'quick-sessions')
34
+ let sessionDirs = []
35
+ try { sessionDirs = readdirSync(sessionsDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) } catch {}
36
+ for (const sid of sessionDirs) {
37
+ if (sid === currentChangeName) continue
38
+ try {
39
+ const guard = JSON.parse(readFileSync(join(sessionsDir, sid, 'guard.json'), 'utf8'))
40
+ for (const f of Array.isArray(guard.allowedFiles) ? guard.allowedFiles : []) add(f, sid)
41
+ } catch { /* 损坏/缺失跳过 */ }
42
+ }
43
+ // ② 其他变更的 design §6 清单(目录在即视为在途变更;archive/ 下不算)
44
+ const changesDir = join(specBase, 'changes')
45
+ let changeDirs = []
46
+ try { changeDirs = readdirSync(changesDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) } catch {}
47
+ for (const cn of changeDirs) {
48
+ if (cn === currentChangeName || cn === 'archive') continue
49
+ const designPath = join(changesDir, cn, 'design.md')
50
+ if (!existsSync(designPath)) continue
51
+ try {
52
+ for (const e of parseFileChangeListDetailed(designPath)) add(e.path, cn)
53
+ } catch { /* 清单解析失败跳过该变更 */ }
54
+ }
55
+ } catch { /* 整体异常 → 空集(等价不过滤,回到原行为) */ }
56
+ return foreign
57
+ }
58
+
59
+ /**
60
+ * 按他者声明集切分 diff 文件(collectForeignDeclaredFiles 的消费端快捷封装)。
61
+ * @param {string} cwd 项目根
62
+ * @param {string|null} currentChangeName 本变更名
63
+ * @param {string[]|null} diffFiles 待切分文件列表(null 透传,保持调用方 null 语义)
64
+ * @returns {{ own: string[]|null, foreign: Array<{file: string, owners: string[]}> }}
65
+ */
66
+ export function splitOwnVsForeignDiffFiles(cwd, currentChangeName, diffFiles) {
67
+ if (!Array.isArray(diffFiles)) return { own: diffFiles, foreign: [] }
68
+ const foreignMap = collectForeignDeclaredFiles(cwd, currentChangeName)
69
+ if (foreignMap.size === 0) return { own: diffFiles, foreign: [] }
70
+ const own = []
71
+ const foreign = []
72
+ for (const f of diffFiles) {
73
+ const owners = foreignMap.get(String(f).replace(/\\/g, '/'))
74
+ if (owners) foreign.push({ file: f, owners })
75
+ else own.push(f)
76
+ }
77
+ return { own, foreign }
78
+ }
package/src/index.js CHANGED
@@ -85,6 +85,7 @@ SillySpec CLI — 规范驱动开发工具包
85
85
  sillyspec config cat [--json] 读取 local.yaml 实际内容(自动定位真实路径,worktree 内解析到主仓)
86
86
  sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
87
87
  sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
88
+ sillyspec agent-log [--detect] [--json] 本地 agent 会话日志查询/现场探测(run 命令自动上报平台 POST /api/agent-logs 并本地留底)
88
89
 
89
90
  sillyspec workflow check <name> [--project <p>] [--change <c>] [--json] [--save]
90
91
  全局 workflow 校验(--save 归档到 .sillyspec/.runtime/workflow-runs/)
@@ -99,6 +100,7 @@ SillySpec CLI — 规范驱动开发工具包
99
100
  sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
100
101
  sillyspec endpoints extract --change <name> [--task task-NN] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
101
102
  sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
103
+ sillyspec plan-adopt-waves --change <名> [--dry-run] plan.md Wave 段一键重排为 depends_on 拓扑分组(同步任务总表 W 列,幂等)
102
104
  sillyspec workspace add <名> <路径> [--role] [--repo] | remove <名> | status 多项目工作区登记与状态探测
103
105
  sillyspec scan-fix-leak --spec-dir <specRoot> source_root 误写产物搬到平台 specRoot(污染一键修复)
104
106
  sillyspec run quick --cancel --change <会话ID> [--ql <ql-id>] 取消误启动的 quick 会话(QUICKLOG 翻已取消 + 挂载行移除)
@@ -777,15 +779,22 @@ async function main() {
777
779
  process.exit(2);
778
780
  }
779
781
  assertSafeChangeName(vpChange, '--change 变更名');
780
- const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton } = await import('./verify-probes.js');
781
- const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir });
782
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase } = await import('./verify-probes.js');
783
+ // spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
784
+ // 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
785
+ // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
786
+ // 才把它的返回值当平台根——无 pointer 时它回退 resolveSpecDir(cwd),会返回 worktree 副本
787
+ // 根,当作 platformBase 传入会让锚定被跳过(骨架落回副本)。
788
+ const vpResolved = resolvePlatformSpecDir(dir, specDir);
789
+ const vpPlatformBase = existsSync(join(dir, '.sillyspec-platform.json')) ? vpResolved : null;
790
+ const vpSpecBase = resolveVerifyProbesSpecBase(dir, specDir, vpPlatformBase);
791
+ const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir: vpSpecBase });
782
792
  if (json) {
783
793
  console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
784
794
  break;
785
795
  }
786
796
  console.log(renderVerifyProbesReport(vpResult));
787
797
  if (vpInit) {
788
- const vpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
789
798
  const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
790
799
  if (existsSync(vpReportPath)) {
791
800
  console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
@@ -796,6 +805,52 @@ async function main() {
796
805
  }
797
806
  break;
798
807
  }
808
+ case 'plan-adopt-waves': {
809
+ // plan.md Wave 段一键重排(坑 wave-manual-mismatch-noise,2026-08-24 用户反馈二期):
810
+ // 手排 Wave 与拓扑比对不一致需手改;依赖方向硬拦(plan-postcheck)落地后给一条零手写出路。
811
+ // depMap 与 postcheck 同源(collectTaskDepMap)→ topoSortWaves → 重写 Wave 段 + 同步任务总表 W 列。
812
+ const pawChangeIdx = args.indexOf('--change');
813
+ const pawChange = pawChangeIdx >= 0 && args[pawChangeIdx + 1] ? args[pawChangeIdx + 1] : null;
814
+ const pawDryRun = args.includes('--dry-run');
815
+ if (!pawChange) {
816
+ console.error('用法: sillyspec plan-adopt-waves --change <name> [--dry-run] [--json] [--spec-dir <path>]\n 把 plan.md 的 Wave 段一键重排为 depends_on 拓扑分组(同步任务总表 W 列;--dry-run 只看不写)');
817
+ process.exit(2);
818
+ }
819
+ assertSafeChangeName(pawChange, '--change 变更名');
820
+ const { adoptPlanWaves } = await import('./plan-adopt-waves.js');
821
+ const pawSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
822
+ const pawResult = adoptPlanWaves({ changeDir: join(pawSpecBase, 'changes', pawChange), dryRun: pawDryRun });
823
+ if (json) {
824
+ console.log(JSON.stringify({ command: 'plan-adopt-waves', change: pawChange, ...pawResult }, null, 2));
825
+ break;
826
+ }
827
+ if (!pawResult.ok) {
828
+ console.error(`❌ ${pawResult.error}`);
829
+ process.exit(1);
830
+ }
831
+ console.log('📊 拓扑分组(depends_on):');
832
+ pawResult.waves.forEach((wave, i) => {
833
+ console.log(` Wave ${i + 1}: ${wave.join(', ')}`);
834
+ });
835
+ if (pawDryRun) {
836
+ console.log(`\n👁️ dry-run——将写入 ${pawResult.planPath} 的 Wave 段预览:`);
837
+ for (const l of pawResult.waveBlock) console.log(` ${l}`);
838
+ } else {
839
+ console.log(`\n✅ plan.md Wave 段已重排: ${pawResult.planPath}(任务总表 W 列更新 ${pawResult.tableRowsUpdated} 行${pawResult.tableRowsSkipped > 0 ? `,${pawResult.tableRowsSkipped} 行非模板格式未改` : ''};行尾统一 LF)`);
840
+ }
841
+ if (pawResult.postcheck) {
842
+ const pc = pawResult.postcheck;
843
+ if ((pc.errors || []).length > 0) {
844
+ console.warn(`⚠️ 重排后 blueprint 一致性问题(topo 只看依赖不看文件重叠——同 Wave 共享路径需手动拆分或补 depends_on):`);
845
+ for (const e of pc.errors.slice(0, 10)) console.warn(` ${e}`);
846
+ if (pc.errors.length > 10) console.warn(` …等共 ${pc.errors.length} 条`);
847
+ } else {
848
+ console.log('✅ 重排后 blueprint 一致性校验通过(无同 Wave 共享路径冲突)');
849
+ }
850
+ for (const w of (pc.warnings || []).slice(0, 5)) console.warn(`⚠️ ${w}`);
851
+ }
852
+ break;
853
+ }
799
854
  case 'module-impact': {
800
855
  // archive 模块影响矩阵骨架(2026-08-21 审计第三批 D3):文件×模块归属按 _module-map.yaml
801
856
  // paths 前缀匹配预填(机械),影响类型/review 标记留 <!--TODO-->(语义)。已存在不覆盖。
@@ -1546,11 +1601,12 @@ SillySpec worktree — git worktree 隔离管理
1546
1601
  }
1547
1602
  case 'apply': {
1548
1603
  if (!wtName) {
1549
- console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge]');
1604
+ console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap]');
1550
1605
  process.exit(1);
1551
1606
  }
1552
1607
  const checkOnly = args.includes('--check-only');
1553
1608
  const merge = args.includes('--merge');
1609
+ const skipOverlap = args.includes('--skip-overlap');
1554
1610
 
1555
1611
  // 解析 --base 参数(默认 merge-base)
1556
1612
  let base = 'merge-base';
@@ -1564,7 +1620,7 @@ SillySpec worktree — git worktree 隔离管理
1564
1620
  base = baseVal;
1565
1621
  }
1566
1622
 
1567
- const { applyWorktree } = await import('./worktree-apply.js');
1623
+ const { applyWorktree, withMainRepoLock } = await import('./worktree-apply.js');
1568
1624
  // W3 task-09:apply 链路构造 ctx(D-013),让 task-05 applyWorktree 按 ctx 区分主仓 A5 / 跨仓 no-op。
1569
1625
  // 跨仓 apply=no-op(G1 D-009):校验 review.head 是跨仓真实 commit + 跳过 wm.cleanup。
1570
1626
  // ctx 构造 fail-closed(约束②)——跨仓配置错时 apply 必须阻断(走错仓=数据所有权事故),不降级。
@@ -1576,7 +1632,17 @@ SillySpec worktree — git worktree 隔离管理
1576
1632
  console.error(`❌ apply 失败:跨仓 MultiRepoContext 构造失败(${e.message})`);
1577
1633
  process.exit(1);
1578
1634
  }
1579
- const result = applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx });
1635
+ // 主仓 apply 互斥锁(坑 main-apply-no-mutex):真 apply(非 checkOnly)在锁内跑——
1636
+ // apply 链(rollback/merge/cleanup)直接改主仓工作区,两会话并发互踩会互相清文件。
1637
+ let result;
1638
+ try {
1639
+ result = checkOnly
1640
+ ? applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap })
1641
+ : await withMainRepoLock(dir, wtName, 'apply', () => applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap }));
1642
+ } catch (lockErr) {
1643
+ console.error(`❌ ${lockErr.message}`);
1644
+ process.exit(1);
1645
+ }
1580
1646
 
1581
1647
  if (result.errors.length > 0) {
1582
1648
  console.error(`❌ 校验失败:`);
@@ -1684,8 +1750,15 @@ SillySpec worktree — git worktree 隔离管理
1684
1750
  console.log('');
1685
1751
  if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
1686
1752
  console.log('Action: auto-applying...');
1687
- const { applyWorktree } = await import('./worktree-apply.js');
1688
- const applyResult = applyWorktree(wtName, { cwd: dir, ctx: _assessCtx });
1753
+ const { applyWorktree, withMainRepoLock } = await import('./worktree-apply.js');
1754
+ // 主仓互斥锁(与手动 apply 同款):自动 apply 同样改主仓工作区,必须互斥
1755
+ let applyResult;
1756
+ try {
1757
+ applyResult = await withMainRepoLock(dir, wtName, 'assess-auto-apply', () => applyWorktree(wtName, { cwd: dir, ctx: _assessCtx }));
1758
+ } catch (lockErr) {
1759
+ console.error(`❌ 自动 apply 未执行:${lockErr.message}`);
1760
+ break;
1761
+ }
1689
1762
  if (applyResult.errors.length > 0) {
1690
1763
  console.error('❌ apply 失败:', applyResult.errors.join('; '));
1691
1764
  } else {
@@ -1784,7 +1857,10 @@ SillySpec worktree — git worktree 隔离管理
1784
1857
  }
1785
1858
  const forceFlag = args.includes('--force');
1786
1859
  try {
1787
- const result = wm.cleanup(wtName, { force: forceFlag });
1860
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):cleanup 删 worktree 注册表/分支/目录,
1861
+ // 与并行会话的 apply/cleanup 互踩会互相清——与其他写主仓操作共用一把 main-repo.lock
1862
+ const { withMainRepoLock } = await import('./worktree-apply.js');
1863
+ const result = await withMainRepoLock(dir, wtName, 'worktree-cleanup', () => wm.cleanup(wtName, { force: forceFlag }));
1788
1864
  if (result.result === 'cleaned' || result.result === 'force-cleaned') {
1789
1865
  console.log(`✅ worktree 已清理: ${wtName} (mode: ${result.mode})`);
1790
1866
  if (result.details?.length > 0) {
@@ -1797,6 +1873,11 @@ SillySpec worktree — git worktree 隔离管理
1797
1873
  console.log(` 原因: in-place 模式没有隔离目录需要清理`);
1798
1874
  } else if (result.result === 'blocked') {
1799
1875
  console.error(`🚫 拒绝清理:有未落主仓交付变更,请先 sillyspec worktree apply ${wtName} 或 --force`);
1876
+ } else if (result.result === 'partial') {
1877
+ // 坑 ghost-dir-junction-pierce:partial 残留此前落进 else 打成「未找到」,用户误以为
1878
+ // 干净、人工 rm -rf 穿透 junction——必须明示残留 + 安全清理指引
1879
+ console.warn(`⚠️ 部分清理(残留:${(result.residual || []).join('; ') || 'worktree 目录'})`);
1880
+ console.warn(` Windows 手动清理勿 rm -rf(穿透 node_modules junction 删主仓依赖):先 cmd /c rmdir "<worktree>\\node_modules" 解链再删,或 sillyspec worktree doctor --fix`);
1800
1881
  } else {
1801
1882
  console.log(`ℹ️ worktree 未找到: ${wtName}`);
1802
1883
  }
@@ -1843,6 +1924,13 @@ SillySpec worktree — git worktree 隔离管理
1843
1924
  }
1844
1925
  break;
1845
1926
  }
1927
+ case 'agent-log': {
1928
+ // 本地 agent 会话日志路径查询/探测(平台会话解析入口):读 <runtimeRoot>/agent-session-log.json
1929
+ //(run 命令在 agent 环境内自动登记),--detect 现场探测不落盘。协议见 docs/platform-agent-log-protocol.md。
1930
+ const { cmdAgentLog } = await import('./agent-session-log.js');
1931
+ await cmdAgentLog(filteredArgs.slice(1), { json, cwd: dir, specDir });
1932
+ break;
1933
+ }
1846
1934
  case 'dispatch': {
1847
1935
  // SillyHub 派发抽象层的 agent 调用桥(design.md §Phase2 / D-007@v1):
1848
1936
  // 仅做能力探测(probe)与派发策略生成(hint),**不执行任何 tool 调用**——
@@ -2696,6 +2784,7 @@ SillySpec config — local.yaml 配置键速查
2696
2784
  'quick-guard.json': 'quick baseline 守卫(旧位置)',
2697
2785
  'worktrees': 'worktree meta(hooks 侧)',
2698
2786
  'user-inputs.md': '步骤输出/输入历史追加日志',
2787
+ 'agent-session-log.json': '本地 agent 会话日志登记留底(run 命令自动 REST 上报平台 POST /api/agent-logs,见 docs/platform-agent-log-protocol.md)',
2699
2788
  'current-quick-run-id': '当前 quick sessionId 指针(--done 无 --change 时 fallback)',
2700
2789
  'scan-runs': '平台模式 scan/workflow 取证目录',
2701
2790
  'artifacts': '派生产物目录(init 预建)',
@@ -2733,7 +2822,7 @@ SillySpec config — local.yaml 配置键速查
2733
2822
  break;
2734
2823
  }
2735
2824
  default: {
2736
- const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
2825
+ const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
2737
2826
  const suggestion = didYouMean(command, topCommands);
2738
2827
  console.error(`❌ 未知命令: ${command}`);
2739
2828
  if (command === '--status') {
package/src/init.js CHANGED
@@ -23,6 +23,9 @@ const __dirname = dirname(__filename);
23
23
  // 详见 docs/sillyspec/runtime-cleanup-destroys-worktree-meta.md
24
24
  //
25
25
  // 策略:白名单保留权威状态,逐项删除可重建的缓存子项;未知子项默认保留(安全侧倾斜)。
26
+ // local.yaml 同样保留:gitignored 凭据文件(平台 init lease 第 5 步下发 / local detect /
27
+ // platform connect 写入,含用户手调 mcp 段),删除即永久丢失无法从 git 找回。本函数曾把它
28
+ // 当非权威残留整删,与 platformMode 跳过清理的保护语义自相矛盾,2026-08-23 起不再删。
26
29
  const RUNTIME_KEEP = new Set([
27
30
  'worktrees', // worktree 目录 + meta.json(worktree.js:17)
28
31
  'sillyspec.db', // SQLite 进度库(权威状态源,progress.js:7)
@@ -33,15 +36,14 @@ const RUNTIME_KEEP = new Set([
33
36
 
34
37
  /**
35
38
  * 清理 .sillyspec/ 下的运行时残留,保留权威状态。
36
- * 同时清理 local.yaml、codebase/(这些非权威,整删安全)。
39
+ * 同时清理 codebase/(非权威,整删安全)。local.yaml 受保护不删——
40
+ * 平台 init 下发的凭据配置,见上方策略注释。
37
41
  * @param {string} legacyDir - 源码目录的 .sillyspec/ 路径
38
42
  */
39
43
  export function cleanupRuntimeResidue(legacyDir) {
40
- // local.yaml、codebase/ 非权威,整删
41
- for (const residue of ['local.yaml', 'codebase']) {
42
- const p = join(legacyDir, residue);
43
- if (existsSync(p)) { try { rmSync(p, { recursive: true, force: true }) } catch {} }
44
- }
44
+ // codebase/ 非权威,整删;local.yaml 是凭据配置,保留
45
+ const codebasePath = join(legacyDir, 'codebase');
46
+ if (existsSync(codebasePath)) { try { rmSync(codebasePath, { recursive: true, force: true }) } catch {} }
45
47
  // .runtime/ 逐项清理,白名单保留
46
48
  const runtimeDir = join(legacyDir, '.runtime');
47
49
  if (existsSync(runtimeDir)) {
@@ -251,7 +253,7 @@ function doInstall(projectDir, tools, subprojects = [], specDir = null, options
251
253
  // 真实资产存在:拒绝整体删除,仅清理运行时残留
252
254
  console.error('❌ [sillyspec] 拒绝删除源码目录的 .sillyspec/:检测到真实资产(changes/、projects/ 或 sillyspec.db)。');
253
255
  console.error(' 该项目似乎本身就用 SillySpec 管理。如需改用外部 spec 目录,请先手动迁移/备份。');
254
- console.error(' 本次仅清理运行时残留(.runtime/ 缓存、local.yaml、codebase/),保留 worktrees 与进度状态。');
256
+ console.error(' 本次仅清理运行时残留(.runtime/ 缓存、codebase/),保留 local.yaml、worktrees 与进度状态。');
255
257
  cleanupRuntimeResidue(legacyDir);
256
258
  } else {
257
259
  // 无真实资产:确属旧版本残留,安全删除
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * knowledge-match.js — knowledge 关键词匹配引擎
3
- * 从 INDEX.md 解析知识条目,按任务上下文匹配并生成 hit report
3
+ * 从 INDEX.md 解析知识条目,按任务上下文匹配并生成 hit report;
4
+ * 扫描范围含 decisions 决策库(knowledge/decisions/<域>.md,经 INDEX.md ## Decisions 段
5
+ * 路由行发现——路由行由 decision-distill 幂等写入,本引擎只消费不写)——命中路由行的
6
+ * 决策文件解析为 decisionHits(rejected 优先),供 brainstorm Step2 防复潮注入(task-04,FR-05)。
7
+ * decisions 库不存在时(INDEX 无 Decisions 段/文件不存在)一切行为与无 decisions 库时一致。
4
8
  */
5
9
 
6
10
  import { existsSync, readFileSync } from 'fs'
@@ -41,6 +45,86 @@ export function parseKnowledgeIndex(indexDir) {
41
45
  return entries
42
46
  }
43
47
 
48
+ // ---------------------------------------------------------------------------
49
+ // decisions 决策库扫描(task-04,FR-05):经 INDEX.md ## Decisions 段路由行发现
50
+ // knowledge/decisions/<域>.md 并解析 D-xxx@vN 条目。条目格式与 decision-distill 的
51
+ // renderBlockLines 逐字对齐(字段行是机械解析契约):`## D-xxx@vN <短标题>` +
52
+ // `状态:implemented|rejected` / `锚点:` / `最近确认:` / `理由:`(rejected 追加
53
+ // `否决理由:` / `复潮条件:`)。
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /** INDEX 条目是否为 decisions 库路由行(目标文件在 decisions/ 下) */
57
+ function isDecisionRoute(entry) {
58
+ return /^decisions\//.test(String(entry.file || '').replace(/\\/g, '/'))
59
+ }
60
+
61
+ const DECISION_HEADER_RE = /^##\s+(D-\d+@v\d+)\s*(.*)$/
62
+ const DECISION_FIELD_RE = /^(状态|理由|否决理由|复潮条件)\s*[::]\s*(.*)$/
63
+
64
+ /**
65
+ * 解析单个 decisions/<域>.md 的 D-xxx@vN 条目。
66
+ * reason 语义:rejected 条目取「否决理由」(expects_from task-02 的 reject_reason),
67
+ * 其余条目回退「理由」一句话;revisitWhen 仅 rejected 条目非空(复潮条件)。
68
+ * @param {string} filePath 决策文件绝对路径
69
+ * @param {string} file INDEX 相对路径(decisions/<域>.md),原样进 decisionHits.file
70
+ * @returns {{ file: string, id: string, title: string, status: string, reason: string, revisitWhen: string }[]}
71
+ */
72
+ function parseDecisionFile(filePath, file) {
73
+ let content
74
+ try { content = readFileSync(filePath, 'utf8') } catch { return [] }
75
+ const hits = []
76
+ let cur = null
77
+ const flush = () => {
78
+ if (cur) hits.push(cur)
79
+ cur = null
80
+ }
81
+ for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
82
+ const h = line.match(DECISION_HEADER_RE)
83
+ if (h) {
84
+ flush()
85
+ cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '' }
86
+ continue
87
+ }
88
+ if (!cur) continue
89
+ const f = line.match(DECISION_FIELD_RE)
90
+ if (!f) continue
91
+ const value = f[2].trim()
92
+ if (f[1] === '状态') cur.status = value.toLowerCase()
93
+ else if (f[1] === '否决理由') cur.reason = value
94
+ else if (f[1] === '复潮条件') cur.revisitWhen = value
95
+ else if (!cur.reason) cur.reason = value // 理由:仅作 implemented(或否决理由缺失)时的回填
96
+ }
97
+ flush()
98
+ return hits
99
+ }
100
+
101
+ /**
102
+ * 解析 knowledge/decisions/ 决策库——扫描范围扩到 decisions/*.md,但只经 INDEX.md
103
+ * Decisions 段路由行发现目标文件(路由行由 decision-distill 幂等维护)。
104
+ * @param {string} indexDir knowledge 目录路径
105
+ * @param {Array} [indexEntries] parseKnowledgeIndex 输出(缺省时自行解析 INDEX;传入时
106
+ * 只解析这些条目中指向 decisions/ 的路由行,matchKnowledge 用它做「路由行已命中」过滤)
107
+ * @returns {{ file: string, id: string, title: string, status: string, reason: string, revisitWhen: string }[]}
108
+ * 路由行不存在/目标文件不存在/解析失败 → [](与无 decisions 库时行为完全一致)
109
+ */
110
+ export function parseDecisionEntries(indexDir, indexEntries = null) {
111
+ const routes = (indexEntries || parseKnowledgeIndex(indexDir)).filter(isDecisionRoute)
112
+ const hits = []
113
+ for (const route of routes) {
114
+ const filePath = join(indexDir, route.file)
115
+ if (!existsSync(filePath)) continue // 路由行失效(域文件已删)→ 跳过不阻断
116
+ hits.push(...parseDecisionFile(filePath, route.file))
117
+ }
118
+ // 多条路由行指向同一域文件时按 file+id 去重
119
+ const seen = new Set()
120
+ return hits.filter(h => {
121
+ const key = `${h.file}#${h.id}`
122
+ if (seen.has(key)) return false
123
+ seen.add(key)
124
+ return true
125
+ })
126
+ }
127
+
44
128
  function escapeRegex(s) {
45
129
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
46
130
  }
@@ -62,7 +146,11 @@ function keywordMatchesContext(keyword, contextLower) {
62
146
  * 用任务上下文匹配知识条目
63
147
  * @param {string} indexDir - knowledge 目录路径
64
148
  * @param {string} taskContext - 任务上下文(task 名称 + 描述,用于关键词匹配)
65
- * @returns {{ matched: boolean, entries: Array, report: string, json: object }}
149
+ * @returns {{ matched: boolean, entries: Array, report: string, json: object,
150
+ * decisionHits: Array<{ file: string, id: string, title: string, status: string, reason: string, revisitWhen: string }> }}
151
+ * matched/entries/report/json 既有四字段结构与语义不变;decisionHits(task-04 新增)= 任务上下文
152
+ * 命中的 Decisions 路由行所指向 decisions/<域>.md 内的全部 D-xxx@vN 条目,rejected 优先排序;
153
+ * 无 decisions 库 / 路由行未命中 → decisionHits: [](其余字段行为与现状一致)。
66
154
  */
67
155
  export function matchKnowledge(indexDir, taskContext) {
68
156
  const indexPath = join(indexDir, 'INDEX.md')
@@ -73,7 +161,8 @@ export function matchKnowledge(indexDir, taskContext) {
73
161
  matched: false,
74
162
  entries: [],
75
163
  report: 'Status: no matches (INDEX.md not found)',
76
- json: { matched: false, entry_count: 0, entries: [] }
164
+ json: { matched: false, entry_count: 0, entries: [] },
165
+ decisionHits: []
77
166
  }
78
167
  }
79
168
 
@@ -83,7 +172,8 @@ export function matchKnowledge(indexDir, taskContext) {
83
172
  matched: false,
84
173
  entries: [],
85
174
  report: 'Status: no matches',
86
- json: { matched: false, entry_count: 0, entries: [] }
175
+ json: { matched: false, entry_count: 0, entries: [] },
176
+ decisionHits: []
87
177
  }
88
178
  }
89
179
 
@@ -97,7 +187,8 @@ export function matchKnowledge(indexDir, taskContext) {
97
187
  matched: false,
98
188
  entries: [],
99
189
  report: 'Status: no matches',
100
- json: { matched: false, entry_count: 0, entries: [] }
190
+ json: { matched: false, entry_count: 0, entries: [] },
191
+ decisionHits: []
101
192
  }
102
193
  }
103
194
 
@@ -126,5 +217,15 @@ export function matchKnowledge(indexDir, taskContext) {
126
217
  }))
127
218
  }
128
219
 
129
- return { matched: true, entries: matched, report, json }
220
+ // decisionHits(task-04):任务上下文命中的 Decisions 路由行 → 解析其指向的
221
+ // decisions/<域>.md 内全部条目;rejected 优先排序(防复潮信息最先可见,组内保持文件序)。
222
+ // 不引入新顶层 hits 字段,不改 matched/entries/report/json 四个既有键。
223
+ const decisionRoutes = matched.filter(isDecisionRoute)
224
+ const allDecisionHits = decisionRoutes.length > 0 ? parseDecisionEntries(indexDir, decisionRoutes) : []
225
+ const decisionHits = [
226
+ ...allDecisionHits.filter(h => h.status === 'rejected'),
227
+ ...allDecisionHits.filter(h => h.status !== 'rejected')
228
+ ]
229
+
230
+ return { matched: true, entries: matched, report, json, decisionHits }
130
231
  }
@@ -44,7 +44,9 @@ export function registerRepoInLocalYaml(yamlPath, key, repoPath) {
44
44
 
45
45
  const existed = existsSync(yamlPath)
46
46
  // CRLF 归一后按 LF 写回(同 task 卡锚点写入的 CRLF 坑;local.yaml 各 CLI 写入口径均 LF)
47
- const raw = existed ? readFileSync(yamlPath, 'utf8').replace(/\r\n?/g, '\n') : ''
47
+ const raw0 = existed ? readFileSync(yamlPath, 'utf8') : ''
48
+ const hadCr = raw0.includes('\r') // 磁盘原文带 CRLF → 幂等路径也要落盘治愈(见下方幂等分支)
49
+ const raw = existed ? raw0.replace(/\r\n?/g, '\n') : ''
48
50
  const posixPath = String(repoPath).replace(/\\/g, '/')
49
51
  const entryLine = ` ${key}: ${posixPath}`
50
52
 
@@ -79,6 +81,10 @@ export function registerRepoInLocalYaml(yamlPath, key, repoPath) {
79
81
  for (let i = reposIdx + 1; i < end; i++) {
80
82
  if (keyRe.test(lines[i])) {
81
83
  if (lines[i] === entryLine) {
84
+ // 坑 register-repo-crlf-idempotent-loop:磁盘原文带 CRLF 时(agent Write 工具/Windows
85
+ // 编辑器写入),内存归一后比对相等直接跳过 → 磁盘永不治愈 + CLI 报 ✅ 假成功 → 解析侧
86
+ // (修复前)空 Map → MultiRepoContext 报「未注册」→ 死循环。幂等路径也落盘一次治愈。
87
+ if (hadCr) write(lines.join('\n'))
82
88
  return { fileCreated: false, sectionCreated: false, replaced: false } // 值未变,幂等跳过
83
89
  }
84
90
  lines[i] = entryLine
@@ -20,6 +20,11 @@
20
20
  * - 判定语义(哪个模块才是「对的」)仍归 agent——本表只是机械最优匹配
21
21
  */
22
22
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
23
+
24
+ /** 模块卡字数预算(字节):超限在 resolve 表标 ⚠️ 并提示精简——文档是 agent 的读取税
25
+ * (对标 deepseek-harness verify-doc-budgets 理念:预算只降不升;2026-08-24 起 runtime.md
26
+ * 28KB/worktree.md 24KB 已在付税)。advisory 不阻断,16 为 dogfood 校准前缺省。 */
27
+ const MODULE_CARD_BUDGET_BYTES = 16 * 1024
23
28
  import { join, relative } from 'path'
24
29
  import { parseModuleMapSimple } from './modules.js'
25
30
  import { parseTaskRegistry } from './stages/execute.js'
@@ -197,9 +202,12 @@ export function renderModuleResolveTable({ cwd, specBase, changeName }) {
197
202
  '| task | 模块 | 粒度 | 模块卡(只读命中的卡,勿按目录整读) | 体量 | 读取建议 |',
198
203
  '|---|---|---|---|---|---|',
199
204
  ]
205
+ const budgetBreached = new Set()
200
206
  for (const r of rows) {
201
207
  if (r.moduleId) {
202
- L.push(`| ${r.taskId} | ${r.moduleId} | ${r.granularity === 'fine' ? '细' : '粗'} | \`${r.cardPath}\` | ${kb(r.cardBytes)} | ${r.advice} |`)
208
+ const over = r.cardBytes > MODULE_CARD_BUDGET_BYTES
209
+ if (over) budgetBreached.add(r.moduleId)
210
+ L.push(`| ${r.taskId} | ${r.moduleId} | ${r.granularity === 'fine' ? '细' : '粗'} | \`${r.cardPath}\` | ${kb(r.cardBytes)}${over ? ' ⚠️超预算' : ''} | ${r.advice} |`)
203
211
  } else {
204
212
  L.push(`| ${r.taskId} | — | — | (无命中,跳过模块文档) | — | — |`)
205
213
  }
@@ -207,5 +215,8 @@ export function renderModuleResolveTable({ cwd, specBase, changeName }) {
207
215
  L.push('')
208
216
  L.push('> 匹配规则:全部 _module-map.yaml 跨层最长前缀(子项目细卡优先于根层大卡),源=tasks 卡 allowed_paths。')
209
217
  L.push('> 细卡整卡可读;仅粗卡命中且体量大时**按节读**——只读「契约摘要/注意事项/定位」,跳过「变更索引/人工备注」历史累积段。')
218
+ if (budgetBreached.size > 0) {
219
+ L.push(`> ⚠️ 模块卡超预算(>${Math.round(MODULE_CARD_BUDGET_BYTES / 1024)}KB):${[...budgetBreached].join('、')}——文档是 agent 的读取税,本次变更触及该模块,收尾时顺手 \`sillyspec modules split-changelog\` 迁出历史段或精简正文(对标 deepseek-harness verify-doc-budgets:预算只降不升)。`)
220
+ }
210
221
  return L.join('\n')
211
222
  }
package/src/modules.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { gitQuiet } from './git-helper.js';
4
+ import { nowWallClock } from './datetime.js';
4
5
 
5
6
 
6
7
  /**
@@ -112,7 +113,7 @@ export async function rebuildModuleMap(cwd, { force = false } = {}) {
112
113
 
113
114
  // 生成新的 _module-map.yaml
114
115
  // 保留已有的完整字段,补齐新卡片
115
- const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
116
+ const now = nowWallClock();
116
117
  let headCommit = '';
117
118
  try {
118
119
  // QUAL-01 收口:走 git-helper 统一入口(safe.directory + 数组形式)