sillyspec 3.27.2 → 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 (221) 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/eval/README.md +86 -0
  147. package/eval/eval.config.json +28 -0
  148. package/eval/lib/adapters/cli.mjs +63 -0
  149. package/eval/lib/adapters/dryrun.mjs +27 -0
  150. package/eval/lib/config.mjs +82 -0
  151. package/eval/lib/db.mjs +76 -0
  152. package/eval/lib/report.mjs +120 -0
  153. package/eval/lib/runner.mjs +157 -0
  154. package/eval/lib/tasks.mjs +73 -0
  155. package/eval/lib/util.mjs +107 -0
  156. package/eval/lib/workdir.mjs +30 -0
  157. package/eval/run.mjs +83 -0
  158. package/eval/tasks/001-slugify/files/package.json +6 -0
  159. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  160. package/eval/tasks/001-slugify/task.json +6 -0
  161. package/eval/tasks/001-slugify/verify.mjs +28 -0
  162. package/eval/tasks/002-each-day/files/package.json +6 -0
  163. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  164. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  165. package/eval/tasks/002-each-day/task.json +6 -0
  166. package/eval/tasks/002-each-day/verify.mjs +31 -0
  167. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  168. package/eval/tasks/003-word-report/files/package.json +6 -0
  169. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  170. package/eval/tasks/003-word-report/task.json +6 -0
  171. package/eval/tasks/003-word-report/verify.mjs +38 -0
  172. package/package.json +4 -2
  173. package/src/agent-session-log.js +45 -5
  174. package/src/change-risk-profile.js +83 -14
  175. package/src/config-schema.js +18 -3
  176. package/src/contract-matrix.js +90 -29
  177. package/src/datetime.js +20 -0
  178. package/src/decision-distill.js +517 -0
  179. package/src/dispatch/backends/local-agent.js +4 -4
  180. package/src/docs-check.js +227 -0
  181. package/src/docs-debt.js +187 -24
  182. package/src/doctor-diagnostics.js +102 -2
  183. package/src/foreign-declared.js +78 -0
  184. package/src/index.js +89 -9
  185. package/src/init.js +9 -7
  186. package/src/knowledge-match.js +107 -6
  187. package/src/local-register.js +7 -1
  188. package/src/module-resolve.js +12 -1
  189. package/src/modules.js +2 -1
  190. package/src/plan-adopt-waves.js +126 -0
  191. package/src/quicklog.js +50 -4
  192. package/src/run/command.js +122 -43
  193. package/src/run/complete-handlers.js +48 -19
  194. package/src/run/complete.js +5 -2
  195. package/src/run/gates.js +87 -22
  196. package/src/run/prompt.js +84 -42
  197. package/src/run/quick-audit.js +11 -0
  198. package/src/run/shared.js +152 -10
  199. package/src/run.js +2 -2
  200. package/src/scan-postcheck.js +30 -17
  201. package/src/stage-contract-spec.js +2 -1
  202. package/src/stage-contract.js +68 -4
  203. package/src/stages/archive.js +53 -12
  204. package/src/stages/brainstorm-auto.js +7 -0
  205. package/src/stages/brainstorm.js +11 -0
  206. package/src/stages/doctor.js +157 -1
  207. package/src/stages/execute.js +32 -6
  208. package/src/stages/plan-postcheck.js +76 -17
  209. package/src/stages/plan.js +1 -0
  210. package/src/stages/quick.js +6 -1
  211. package/src/stages/verify.js +31 -14
  212. package/src/sync.js +41 -3
  213. package/src/task-review.js +2 -2
  214. package/src/taskcard-placeholders.js +21 -0
  215. package/src/taskcard.js +10 -1
  216. package/src/verify-postcheck.js +315 -13
  217. package/src/verify-probes.js +46 -4
  218. package/src/worktree-apply.js +184 -7
  219. package/src/worktree-deps.js +57 -0
  220. package/src/worktree.js +201 -58
  221. 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
@@ -100,6 +100,7 @@ SillySpec CLI — 规范驱动开发工具包
100
100
  sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
101
101
  sillyspec endpoints extract --change <name> [--task task-NN] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
102
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 列,幂等)
103
104
  sillyspec workspace add <名> <路径> [--role] [--repo] | remove <名> | status 多项目工作区登记与状态探测
104
105
  sillyspec scan-fix-leak --spec-dir <specRoot> source_root 误写产物搬到平台 specRoot(污染一键修复)
105
106
  sillyspec run quick --cancel --change <会话ID> [--ql <ql-id>] 取消误启动的 quick 会话(QUICKLOG 翻已取消 + 挂载行移除)
@@ -778,15 +779,22 @@ async function main() {
778
779
  process.exit(2);
779
780
  }
780
781
  assertSafeChangeName(vpChange, '--change 变更名');
781
- const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton } = await import('./verify-probes.js');
782
- 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 });
783
792
  if (json) {
784
793
  console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
785
794
  break;
786
795
  }
787
796
  console.log(renderVerifyProbesReport(vpResult));
788
797
  if (vpInit) {
789
- const vpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
790
798
  const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
791
799
  if (existsSync(vpReportPath)) {
792
800
  console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
@@ -797,6 +805,52 @@ async function main() {
797
805
  }
798
806
  break;
799
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
+ }
800
854
  case 'module-impact': {
801
855
  // archive 模块影响矩阵骨架(2026-08-21 审计第三批 D3):文件×模块归属按 _module-map.yaml
802
856
  // paths 前缀匹配预填(机械),影响类型/review 标记留 <!--TODO-->(语义)。已存在不覆盖。
@@ -1547,11 +1601,12 @@ SillySpec worktree — git worktree 隔离管理
1547
1601
  }
1548
1602
  case 'apply': {
1549
1603
  if (!wtName) {
1550
- 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]');
1551
1605
  process.exit(1);
1552
1606
  }
1553
1607
  const checkOnly = args.includes('--check-only');
1554
1608
  const merge = args.includes('--merge');
1609
+ const skipOverlap = args.includes('--skip-overlap');
1555
1610
 
1556
1611
  // 解析 --base 参数(默认 merge-base)
1557
1612
  let base = 'merge-base';
@@ -1565,7 +1620,7 @@ SillySpec worktree — git worktree 隔离管理
1565
1620
  base = baseVal;
1566
1621
  }
1567
1622
 
1568
- const { applyWorktree } = await import('./worktree-apply.js');
1623
+ const { applyWorktree, withMainRepoLock } = await import('./worktree-apply.js');
1569
1624
  // W3 task-09:apply 链路构造 ctx(D-013),让 task-05 applyWorktree 按 ctx 区分主仓 A5 / 跨仓 no-op。
1570
1625
  // 跨仓 apply=no-op(G1 D-009):校验 review.head 是跨仓真实 commit + 跳过 wm.cleanup。
1571
1626
  // ctx 构造 fail-closed(约束②)——跨仓配置错时 apply 必须阻断(走错仓=数据所有权事故),不降级。
@@ -1577,7 +1632,17 @@ SillySpec worktree — git worktree 隔离管理
1577
1632
  console.error(`❌ apply 失败:跨仓 MultiRepoContext 构造失败(${e.message})`);
1578
1633
  process.exit(1);
1579
1634
  }
1580
- 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
+ }
1581
1646
 
1582
1647
  if (result.errors.length > 0) {
1583
1648
  console.error(`❌ 校验失败:`);
@@ -1685,8 +1750,15 @@ SillySpec worktree — git worktree 隔离管理
1685
1750
  console.log('');
1686
1751
  if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
1687
1752
  console.log('Action: auto-applying...');
1688
- const { applyWorktree } = await import('./worktree-apply.js');
1689
- 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
+ }
1690
1762
  if (applyResult.errors.length > 0) {
1691
1763
  console.error('❌ apply 失败:', applyResult.errors.join('; '));
1692
1764
  } else {
@@ -1785,7 +1857,10 @@ SillySpec worktree — git worktree 隔离管理
1785
1857
  }
1786
1858
  const forceFlag = args.includes('--force');
1787
1859
  try {
1788
- 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 }));
1789
1864
  if (result.result === 'cleaned' || result.result === 'force-cleaned') {
1790
1865
  console.log(`✅ worktree 已清理: ${wtName} (mode: ${result.mode})`);
1791
1866
  if (result.details?.length > 0) {
@@ -1798,6 +1873,11 @@ SillySpec worktree — git worktree 隔离管理
1798
1873
  console.log(` 原因: in-place 模式没有隔离目录需要清理`);
1799
1874
  } else if (result.result === 'blocked') {
1800
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`);
1801
1881
  } else {
1802
1882
  console.log(`ℹ️ worktree 未找到: ${wtName}`);
1803
1883
  }
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 + 数组形式)
@@ -0,0 +1,126 @@
1
+ /**
2
+ * plan-adopt-waves.js — `sillyspec plan-adopt-waves` 把 plan.md 的 Wave 段一键重排为
3
+ * depends_on 拓扑分组(坑 wave-manual-mismatch-noise,2026-08-24 用户反馈二期:主控手排
4
+ * Wave 与 CLI 拓扑比对不一致只能手动改;依赖方向违规修复后,本命令给一条零手写出路)。
5
+ *
6
+ * 行为:
7
+ * - depMap(tasks/*.md 的 depends_on,collectTaskDepMap 与 postcheck 同源)→ topoSortWaves
8
+ * - 重写 plan.md 的 `## Wave N` 段块(段内按编号排序;首段「并行,无依赖」、其余「依赖前序 Wave」)
9
+ * - 无显式 Wave 标题 → 插在「## 任务总表」前(无总表则文末追加)
10
+ * - best-effort 同步任务总表的 W 列(仅认 7 列模板行;不匹配的行 warn 不强改)
11
+ * - Wave 段内若含非引用内容(正文/表格)→ 拒绝重写(防误删),提示先手动整理
12
+ * - 写后复跑 validateBlueprintConsistency 打印结果(防 adopt 把手工安全串行收敛出
13
+ * 同 Wave 共享路径冲突——topo 只看依赖不看文件重叠)
14
+ * - --dry-run 只打印将写入的 Wave 段不落盘;幂等(重复跑产出相同结构);行尾统一 LF
15
+ */
16
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
17
+ import { join } from 'path'
18
+ import { topoSortWaves, collectTaskDepMap, validateBlueprintConsistency } from './stages/plan-postcheck.js'
19
+
20
+ const WAVE_HEADING_RE = /^#+\s*Wave\s+\d+/i
21
+ const TASK_REF_RE = /^[-*]\s+(?:\[[ x]\]\s*)?task-\d+\s*[::\s]*$/i
22
+ const LEGACY_TASK_REF_RE = /^[-*]\s*\[[ x]\]\s*task-\d+/i
23
+ const SUMMARY_HEADING_RE = /^#+\s*任务总表/
24
+
25
+ /** 渲染标准 Wave 段块(段落间空行分隔,结尾不带多余空行——由调用方控制拼接) */
26
+ function renderWaveBlock(waves) {
27
+ const L = []
28
+ waves.forEach((wave, i) => {
29
+ const sorted = [...wave].sort((a, b) =>
30
+ parseInt(String(a).replace(/\D/g, ''), 10) - parseInt(String(b).replace(/\D/g, ''), 10))
31
+ L.push(`## Wave ${i + 1}(${i === 0 ? '并行,无依赖' : '依赖前序 Wave'})`)
32
+ for (const t of sorted) L.push(`- ${t}`)
33
+ if (i < waves.length - 1) L.push('')
34
+ })
35
+ return L
36
+ }
37
+
38
+ /** best-effort 更新任务总表 W 列:仅认 `| task-XX | … | W1 | … |` 形态的 7+ 列模板行 */
39
+ function updateSummaryWaveColumn(lines, waveOf) {
40
+ let inSummary = false
41
+ let updated = 0
42
+ let skipped = 0
43
+ for (let i = 0; i < lines.length; i++) {
44
+ const line = lines[i]
45
+ if (/^#+\s/.test(line)) { inSummary = SUMMARY_HEADING_RE.test(line); continue }
46
+ if (!inSummary) continue
47
+ const m = line.match(/^\|\s*task-(\d+)\s*\|/)
48
+ if (!m) continue
49
+ const cells = line.split('|')
50
+ // 模板 7 列:| 编号 | 任务 | Wave | 优先级 | 依赖 | 覆盖 FR/D | 说明 | → split 后首尾空串
51
+ if (cells.length < 8 || !/^\s*W\d+\s*$/.test(cells[3] || '')) { skipped++; continue }
52
+ const taskId = `task-${String(m[1]).padStart(2, '0')}`
53
+ const waveIdx = waveOf.get(taskId)
54
+ if (waveIdx === undefined) { skipped++; continue }
55
+ cells[3] = ` W${waveIdx + 1} `
56
+ lines[i] = cells.join('|')
57
+ updated++
58
+ }
59
+ return { updated, skipped }
60
+ }
61
+
62
+ /**
63
+ * @param {{ changeDir: string, dryRun?: boolean }} opts
64
+ * @returns {{ ok: boolean, error?: string, waves?: string[][], planPath: string, dryRun: boolean,
65
+ * waveBlock?: string[], tableRowsUpdated?: number, tableRowsSkipped?: number,
66
+ * postcheck?: { errors: string[], warnings: string[] } }}
67
+ */
68
+ export function adoptPlanWaves({ changeDir, dryRun = false }) {
69
+ const planPath = join(changeDir, 'plan.md')
70
+ if (!existsSync(planPath)) return { ok: false, error: `plan.md 不存在: ${planPath}`, planPath, dryRun }
71
+ const tasksDir = join(changeDir, 'tasks')
72
+ if (!existsSync(tasksDir)) return { ok: false, error: `tasks/ 目录不存在: ${tasksDir}`, planPath, dryRun }
73
+ const depMap = collectTaskDepMap(tasksDir)
74
+ if (depMap.size === 0) return { ok: false, error: 'tasks/ 下没有 task-NN.md 卡片,无从推导 Wave 分组', planPath, dryRun }
75
+
76
+ const { waves, error: topoError } = topoSortWaves(depMap)
77
+ if (topoError) return { ok: false, error: `拓扑排序失败: ${topoError}`, planPath, dryRun }
78
+
79
+ const original = readFileSync(planPath, 'utf8')
80
+ const lines = original.split(/\r?\n/)
81
+ const waveBlock = renderWaveBlock(waves)
82
+ const waveOf = new Map()
83
+ waves.forEach((ws, i) => { for (const t of ws) waveOf.set(t, i) })
84
+
85
+ const firstWaveIdx = lines.findIndex(l => WAVE_HEADING_RE.test(l))
86
+ if (firstWaveIdx >= 0) {
87
+ // Wave 段块末尾 = 最后一个 Wave 标题之后连续的任务引用行(空行容忍但不断段;
88
+ // 遇非 Wave 标题即止)。段内混入非引用内容 → 拒绝重写(防误删正文/表格)。
89
+ let endIdx = firstWaveIdx
90
+ for (let i = firstWaveIdx; i < lines.length; i++) {
91
+ if (WAVE_HEADING_RE.test(lines[i])) { endIdx = i; continue }
92
+ if (TASK_REF_RE.test(lines[i]) || LEGACY_TASK_REF_RE.test(lines[i])) { endIdx = i; continue }
93
+ if (lines[i].trim() === '') continue
94
+ if (/^#+\s/.test(lines[i])) break
95
+ return { ok: false, error: `Wave 段内含非引用内容(第 ${i + 1} 行「${lines[i].slice(0, 40)}」)——为防误删拒绝重写,请先把 Wave 段整理为纯 "- task-XX" 引用行`, planPath, dryRun }
96
+ }
97
+ lines.splice(firstWaveIdx, endIdx - firstWaveIdx + 1, ...waveBlock)
98
+ } else {
99
+ // 无显式 Wave 标题:插在「## 任务总表」前;无总表则文末追加
100
+ const summaryIdx = lines.findIndex(l => SUMMARY_HEADING_RE.test(l))
101
+ if (summaryIdx >= 0) {
102
+ lines.splice(summaryIdx, 0, ...waveBlock, '')
103
+ } else {
104
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
105
+ lines.push('', ...waveBlock)
106
+ }
107
+ }
108
+
109
+ const { updated, skipped } = updateSummaryWaveColumn(lines, waveOf)
110
+ const content = lines.join('\n').replace(/\n*$/, '\n')
111
+
112
+ if (dryRun) {
113
+ return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped }
114
+ }
115
+
116
+ writeFileSync(planPath, content, 'utf8')
117
+ // 复跑 blueprint 一致性(advisory 打印):topo 只看依赖不看文件重叠——adopt 可能把手工
118
+ // 安全串行收敛出同 Wave 共享路径冲突,立即暴露而非留到 execute 撞
119
+ let postcheck = null
120
+ try {
121
+ postcheck = validateBlueprintConsistency(changeDir)
122
+ } catch (e) {
123
+ postcheck = { errors: [`复跑校验异常: ${e?.message || e}`], warnings: [] }
124
+ }
125
+ return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped, postcheck }
126
+ }