sillyspec 3.27.2 → 3.27.6

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 (398) 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/.worktrees/8d2d0392/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  147. package/.worktrees/8d2d0392/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  148. package/.worktrees/8d2d0392/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  149. package/.worktrees/8d2d0392/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  150. package/.worktrees/8d2d0392/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  151. package/.worktrees/8d2d0392/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  152. package/.worktrees/8d2d0392/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  153. package/.worktrees/8d2d0392/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  154. package/.worktrees/8d2d0392/.claude/skills/sillyspec-export/SKILL.md +57 -0
  155. package/.worktrees/8d2d0392/.claude/skills/sillyspec-init/SKILL.md +177 -0
  156. package/.worktrees/8d2d0392/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  157. package/.worktrees/8d2d0392/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  158. package/.worktrees/8d2d0392/.claude/skills/sillyspec-propose/SKILL.md +87 -0
  159. package/.worktrees/8d2d0392/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  160. package/.worktrees/8d2d0392/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  161. package/.worktrees/8d2d0392/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  162. package/.worktrees/8d2d0392/.claude/skills/sillyspec-state/SKILL.md +64 -0
  163. package/.worktrees/8d2d0392/.claude/skills/sillyspec-status/SKILL.md +54 -0
  164. package/.worktrees/8d2d0392/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  165. package/.worktrees/8d2d0392/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  166. package/.worktrees/8d2d0392/.claude/skills/verify-per-user/SKILL.md +6 -0
  167. package/.worktrees/8d2d0392/.gitattributes +2 -0
  168. package/.worktrees/8d2d0392/.husky/pre-push +20 -0
  169. package/.worktrees/8d2d0392/README.md +203 -0
  170. package/.worktrees/8d2d0392/SKILL.md +91 -0
  171. package/.worktrees/8d2d0392/architecture-analysis.md +448 -0
  172. package/.worktrees/8d2d0392/bin/sillyspec.js +2 -0
  173. package/.worktrees/8d2d0392/eval/README.md +86 -0
  174. package/.worktrees/8d2d0392/eval/eval.config.json +28 -0
  175. package/.worktrees/8d2d0392/eval/lib/adapters/cli.mjs +63 -0
  176. package/.worktrees/8d2d0392/eval/lib/adapters/dryrun.mjs +27 -0
  177. package/.worktrees/8d2d0392/eval/lib/config.mjs +82 -0
  178. package/.worktrees/8d2d0392/eval/lib/db.mjs +76 -0
  179. package/.worktrees/8d2d0392/eval/lib/report.mjs +120 -0
  180. package/.worktrees/8d2d0392/eval/lib/runner.mjs +157 -0
  181. package/.worktrees/8d2d0392/eval/lib/tasks.mjs +73 -0
  182. package/.worktrees/8d2d0392/eval/lib/util.mjs +107 -0
  183. package/.worktrees/8d2d0392/eval/lib/workdir.mjs +30 -0
  184. package/.worktrees/8d2d0392/eval/run.mjs +83 -0
  185. package/.worktrees/8d2d0392/eval/tasks/001-slugify/files/package.json +6 -0
  186. package/.worktrees/8d2d0392/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  187. package/.worktrees/8d2d0392/eval/tasks/001-slugify/task.json +6 -0
  188. package/.worktrees/8d2d0392/eval/tasks/001-slugify/verify.mjs +28 -0
  189. package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/package.json +6 -0
  190. package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  191. package/.worktrees/8d2d0392/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  192. package/.worktrees/8d2d0392/eval/tasks/002-each-day/task.json +6 -0
  193. package/.worktrees/8d2d0392/eval/tasks/002-each-day/verify.mjs +31 -0
  194. package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  195. package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/package.json +6 -0
  196. package/.worktrees/8d2d0392/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  197. package/.worktrees/8d2d0392/eval/tasks/003-word-report/task.json +6 -0
  198. package/.worktrees/8d2d0392/eval/tasks/003-word-report/verify.mjs +38 -0
  199. package/.worktrees/8d2d0392/logo.jpg +0 -0
  200. package/.worktrees/8d2d0392/package-lock.json +977 -0
  201. package/.worktrees/8d2d0392/package.json +41 -0
  202. package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  203. package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  204. package/.worktrees/8d2d0392/packages/dashboard/dist/favicon.jpg +0 -0
  205. package/.worktrees/8d2d0392/packages/dashboard/dist/index.html +16 -0
  206. package/.worktrees/8d2d0392/packages/dashboard/dist/logo.jpg +0 -0
  207. package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-dashboard.html +836 -0
  208. package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-overview.html +256 -0
  209. package/.worktrees/8d2d0392/packages/dashboard/package.json +25 -0
  210. package/.worktrees/8d2d0392/packages/dashboard/server/executor.js +95 -0
  211. package/.worktrees/8d2d0392/packages/dashboard/server/index.js +679 -0
  212. package/.worktrees/8d2d0392/packages/dashboard/server/parser.js +526 -0
  213. package/.worktrees/8d2d0392/packages/dashboard/server/watcher.js +357 -0
  214. package/.worktrees/8d2d0392/src/agent-session-log.js +803 -0
  215. package/.worktrees/8d2d0392/src/change-list.js +290 -0
  216. package/.worktrees/8d2d0392/src/change-risk-profile.js +339 -0
  217. package/.worktrees/8d2d0392/src/check-primitives.js +65 -0
  218. package/.worktrees/8d2d0392/src/classify-change.js +109 -0
  219. package/.worktrees/8d2d0392/src/commit-suggest.js +202 -0
  220. package/.worktrees/8d2d0392/src/config-cat.js +88 -0
  221. package/.worktrees/8d2d0392/src/config-schema.js +328 -0
  222. package/.worktrees/8d2d0392/src/constants.js +97 -0
  223. package/.worktrees/8d2d0392/src/contract-matrix.js +533 -0
  224. package/.worktrees/8d2d0392/src/datetime.js +20 -0
  225. package/.worktrees/8d2d0392/src/db-engine.js +55 -0
  226. package/.worktrees/8d2d0392/src/db.js +381 -0
  227. package/.worktrees/8d2d0392/src/decision-distill.js +517 -0
  228. package/.worktrees/8d2d0392/src/dispatch/backends/local-agent.js +155 -0
  229. package/.worktrees/8d2d0392/src/dispatch/backends/sillyhub-mcp.js +245 -0
  230. package/.worktrees/8d2d0392/src/dispatch/probe.js +246 -0
  231. package/.worktrees/8d2d0392/src/dispatch/strategy.js +121 -0
  232. package/.worktrees/8d2d0392/src/docs-check.js +825 -0
  233. package/.worktrees/8d2d0392/src/docs-debt.js +393 -0
  234. package/.worktrees/8d2d0392/src/docs-gate.js +107 -0
  235. package/.worktrees/8d2d0392/src/doctor-diagnostics.js +1099 -0
  236. package/.worktrees/8d2d0392/src/endpoint-extractor.js +409 -0
  237. package/.worktrees/8d2d0392/src/foreign-declared.js +78 -0
  238. package/.worktrees/8d2d0392/src/fs-atomic.js +73 -0
  239. package/.worktrees/8d2d0392/src/git-helper.js +150 -0
  240. package/.worktrees/8d2d0392/src/hooks/claude-pre-tool-use.cjs +130 -0
  241. package/.worktrees/8d2d0392/src/hooks/worktree-guard.js +892 -0
  242. package/.worktrees/8d2d0392/src/index.js +2854 -0
  243. package/.worktrees/8d2d0392/src/init.js +626 -0
  244. package/.worktrees/8d2d0392/src/knowledge-match.js +231 -0
  245. package/.worktrees/8d2d0392/src/local-detect.js +128 -0
  246. package/.worktrees/8d2d0392/src/local-register.js +104 -0
  247. package/.worktrees/8d2d0392/src/machine-interface.js +543 -0
  248. package/.worktrees/8d2d0392/src/migrate.js +127 -0
  249. package/.worktrees/8d2d0392/src/module-impact.js +166 -0
  250. package/.worktrees/8d2d0392/src/module-resolve.js +222 -0
  251. package/.worktrees/8d2d0392/src/modules.js +580 -0
  252. package/.worktrees/8d2d0392/src/plan-adopt-waves.js +126 -0
  253. package/.worktrees/8d2d0392/src/progress/change-registry.js +336 -0
  254. package/.worktrees/8d2d0392/src/progress/consistency-doctor.js +355 -0
  255. package/.worktrees/8d2d0392/src/progress/shared.js +36 -0
  256. package/.worktrees/8d2d0392/src/progress/stage-machine.js +594 -0
  257. package/.worktrees/8d2d0392/src/progress/step-store.js +265 -0
  258. package/.worktrees/8d2d0392/src/progress.js +1304 -0
  259. package/.worktrees/8d2d0392/src/quick-recommend.js +115 -0
  260. package/.worktrees/8d2d0392/src/quicklog.js +883 -0
  261. package/.worktrees/8d2d0392/src/review-tier.js +77 -0
  262. package/.worktrees/8d2d0392/src/run/command.js +1641 -0
  263. package/.worktrees/8d2d0392/src/run/complete-handlers.js +1552 -0
  264. package/.worktrees/8d2d0392/src/run/complete.js +1259 -0
  265. package/.worktrees/8d2d0392/src/run/concurrent-detect.js +156 -0
  266. package/.worktrees/8d2d0392/src/run/gates.js +1094 -0
  267. package/.worktrees/8d2d0392/src/run/multi-repo-context.js +208 -0
  268. package/.worktrees/8d2d0392/src/run/next.js +128 -0
  269. package/.worktrees/8d2d0392/src/run/prompt.js +843 -0
  270. package/.worktrees/8d2d0392/src/run/quick-audit.js +186 -0
  271. package/.worktrees/8d2d0392/src/run/scan-profile.js +269 -0
  272. package/.worktrees/8d2d0392/src/run/shared.js +1562 -0
  273. package/.worktrees/8d2d0392/src/run/stage.js +587 -0
  274. package/.worktrees/8d2d0392/src/run.js +23 -0
  275. package/.worktrees/8d2d0392/src/scan-diff.js +388 -0
  276. package/.worktrees/8d2d0392/src/scan-postcheck.js +524 -0
  277. package/.worktrees/8d2d0392/src/scan-staleness.js +95 -0
  278. package/.worktrees/8d2d0392/src/setup.js +414 -0
  279. package/.worktrees/8d2d0392/src/sillyhub-mcp/client.js +597 -0
  280. package/.worktrees/8d2d0392/src/sillyhub-mcp/config.js +62 -0
  281. package/.worktrees/8d2d0392/src/spec-dir-typo.js +50 -0
  282. package/.worktrees/8d2d0392/src/spec-sync.js +358 -0
  283. package/.worktrees/8d2d0392/src/stage-contract-engine.js +171 -0
  284. package/.worktrees/8d2d0392/src/stage-contract-spec.js +551 -0
  285. package/.worktrees/8d2d0392/src/stage-contract.js +948 -0
  286. package/.worktrees/8d2d0392/src/stage-review.js +597 -0
  287. package/.worktrees/8d2d0392/src/stages/archive.js +199 -0
  288. package/.worktrees/8d2d0392/src/stages/brainstorm-auto.js +285 -0
  289. package/.worktrees/8d2d0392/src/stages/brainstorm.js +624 -0
  290. package/.worktrees/8d2d0392/src/stages/cmd-existence.js +145 -0
  291. package/.worktrees/8d2d0392/src/stages/doctor.js +540 -0
  292. package/.worktrees/8d2d0392/src/stages/execute.js +1277 -0
  293. package/.worktrees/8d2d0392/src/stages/explore.js +43 -0
  294. package/.worktrees/8d2d0392/src/stages/index.js +29 -0
  295. package/.worktrees/8d2d0392/src/stages/knowledge.js +514 -0
  296. package/.worktrees/8d2d0392/src/stages/plan-postcheck.js +1398 -0
  297. package/.worktrees/8d2d0392/src/stages/plan.js +651 -0
  298. package/.worktrees/8d2d0392/src/stages/quick.js +167 -0
  299. package/.worktrees/8d2d0392/src/stages/scan.js +608 -0
  300. package/.worktrees/8d2d0392/src/stages/status.js +64 -0
  301. package/.worktrees/8d2d0392/src/stages/verify.js +246 -0
  302. package/.worktrees/8d2d0392/src/sync.js +1470 -0
  303. package/.worktrees/8d2d0392/src/task-review.js +1498 -0
  304. package/.worktrees/8d2d0392/src/taskcard-placeholders.js +21 -0
  305. package/.worktrees/8d2d0392/src/taskcard.js +217 -0
  306. package/.worktrees/8d2d0392/src/verify-postcheck.js +1718 -0
  307. package/.worktrees/8d2d0392/src/verify-probes.js +327 -0
  308. package/.worktrees/8d2d0392/src/version.js +19 -0
  309. package/.worktrees/8d2d0392/src/workflow.js +600 -0
  310. package/.worktrees/8d2d0392/src/workspace.js +125 -0
  311. package/.worktrees/8d2d0392/src/worktree-apply.js +1611 -0
  312. package/.worktrees/8d2d0392/src/worktree-deps.js +576 -0
  313. package/.worktrees/8d2d0392/src/worktree.js +1838 -0
  314. package/.worktrees/8d2d0392/templates/claude-instruction.md +23 -0
  315. package/.worktrees/8d2d0392/templates/prompts/taskcard-rules.md +29 -0
  316. package/.worktrees/8d2d0392/templates/prompts/testcase-design.md +12 -0
  317. package/.worktrees/8d2d0392/templates/prompts/verify-probes.md +32 -0
  318. package/.worktrees/8d2d0392/templates/workflows/archive-impact.yaml +79 -0
  319. package/.worktrees/8d2d0392/templates/workflows/scan-docs.yaml +132 -0
  320. package/architecture-analysis.md +448 -0
  321. package/eval/README.md +86 -0
  322. package/eval/eval.config.json +28 -0
  323. package/eval/lib/adapters/cli.mjs +63 -0
  324. package/eval/lib/adapters/dryrun.mjs +27 -0
  325. package/eval/lib/config.mjs +82 -0
  326. package/eval/lib/db.mjs +76 -0
  327. package/eval/lib/report.mjs +120 -0
  328. package/eval/lib/runner.mjs +157 -0
  329. package/eval/lib/tasks.mjs +73 -0
  330. package/eval/lib/util.mjs +107 -0
  331. package/eval/lib/workdir.mjs +30 -0
  332. package/eval/run.mjs +83 -0
  333. package/eval/tasks/001-slugify/files/package.json +6 -0
  334. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  335. package/eval/tasks/001-slugify/task.json +6 -0
  336. package/eval/tasks/001-slugify/verify.mjs +28 -0
  337. package/eval/tasks/002-each-day/files/package.json +6 -0
  338. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  339. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  340. package/eval/tasks/002-each-day/task.json +6 -0
  341. package/eval/tasks/002-each-day/verify.mjs +31 -0
  342. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  343. package/eval/tasks/003-word-report/files/package.json +6 -0
  344. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  345. package/eval/tasks/003-word-report/task.json +6 -0
  346. package/eval/tasks/003-word-report/verify.mjs +38 -0
  347. package/package.json +4 -2
  348. package/src/agent-session-log.js +45 -5
  349. package/src/change-risk-profile.js +83 -14
  350. package/src/config-schema.js +18 -3
  351. package/src/contract-matrix.js +90 -29
  352. package/src/datetime.js +20 -0
  353. package/src/decision-distill.js +517 -0
  354. package/src/dispatch/backends/local-agent.js +4 -4
  355. package/src/docs-check.js +227 -0
  356. package/src/docs-debt.js +187 -24
  357. package/src/doctor-diagnostics.js +102 -2
  358. package/src/endpoint-extractor.js +41 -52
  359. package/src/foreign-declared.js +155 -0
  360. package/src/index.js +95 -13
  361. package/src/init.js +9 -7
  362. package/src/knowledge-match.js +107 -6
  363. package/src/local-register.js +7 -1
  364. package/src/module-resolve.js +12 -1
  365. package/src/modules.js +2 -1
  366. package/src/plan-adopt-waves.js +126 -0
  367. package/src/quicklog.js +50 -4
  368. package/src/run/command.js +124 -44
  369. package/src/run/complete-handlers.js +48 -19
  370. package/src/run/complete.js +5 -2
  371. package/src/run/gates.js +104 -22
  372. package/src/run/prompt.js +84 -42
  373. package/src/run/quick-audit.js +11 -0
  374. package/src/run/shared.js +152 -10
  375. package/src/run/stage.js +8 -5
  376. package/src/run.js +2 -2
  377. package/src/scan-postcheck.js +30 -17
  378. package/src/stage-contract-spec.js +3 -2
  379. package/src/stage-contract.js +68 -4
  380. package/src/stages/archive.js +53 -12
  381. package/src/stages/brainstorm-auto.js +50 -1
  382. package/src/stages/brainstorm.js +13 -0
  383. package/src/stages/doctor.js +157 -1
  384. package/src/stages/execute.js +32 -6
  385. package/src/stages/plan-postcheck.js +76 -17
  386. package/src/stages/plan.js +9 -4
  387. package/src/stages/quick.js +6 -1
  388. package/src/stages/verify.js +32 -15
  389. package/src/sync.js +41 -3
  390. package/src/task-review.js +2 -2
  391. package/src/taskcard-placeholders.js +21 -0
  392. package/src/taskcard.js +43 -1
  393. package/src/verify-postcheck.js +315 -13
  394. package/src/verify-probes.js +73 -10
  395. package/src/worktree-apply.js +569 -227
  396. package/src/worktree-deps.js +150 -1
  397. package/src/worktree.js +285 -77
  398. package/templates/prompts/taskcard-rules.md +2 -1
@@ -0,0 +1,1552 @@
1
+ /**
2
+ * run/complete-handlers.js(W6 Step5 从 run.js 抽出)。
3
+ *
4
+ * completeStep 的子 handler + archive 收尾(自洽叶子模块,仅被 completeStep 调用):
5
+ * - handleArchiveConfirmStep:archive「确认归档」步骤 --confirm 门控 + 推荐文档校验
6
+ * - handlePlanGeneratePlanStep:plan「generate_plan」完成后动态插入 coordinator + postcheck 步骤
7
+ * - handleScanProjectListStep:scan step 2 完成后按项目展开 perProject 步骤(用 sanitizeProjectName/validateParsedProjects)
8
+ * - archiveChangeDirectory:归档移动变更目录(6 处 process.exit(1) + worktree 清理;handleArchiveConfirmStep 内部调用)
9
+ * srcDir 缺失时走 findAlreadyArchivedDir 幂等自愈(issue archive-stage-physical-tracking-desync);
10
+ * archiveChangeDirectory + findAlreadyArchivedDir 已 export 供 test 直接 import
11
+ * - sanitizeProjectName / validateParsedProjects:项目名清洗 + 列表校验纯函数(handleScanProjectListStep 专用)
12
+ *
13
+ * 安全锚:run.js 始终 barrel。3 handler 由 run.js import 回来;sanitizeProjectName + validateParsedProjects
14
+ * 被 test 直接 import(run-sanitize-project-name / run-scan-project-parse),run.js barrel re-export 契约保留。
15
+ * 4 目标 handler 无 test 直接 import;archiveChangeDirectory + findAlreadyArchivedDir 供自愈 test 直接 import,无需 barrel re-export。completeStep(Step7 搬)将把 import 行带走。
16
+ *
17
+ * 路径修正(相对 src/run/):
18
+ * - resolveChangeDir 从 './shared.js';renameSyncRetry 从 '../fs-atomic.js';stageRegistry 从 '../stages/index.js'
19
+ * - 动态 import './stages/plan.js' / './worktree.js' → '../'(src/ 下,退一层;真环依赖保留动态)
20
+ * - 删除 archiveChangeDirectory 内死代码 `const { renameSync } = await import('fs')`(renameSync 解构未用,实际走 renameSyncRetry)
21
+ *
22
+ * archiveChangeDirectory 的 6 处 process.exit 全 exit(1):5 个顶层 guard 直接终止;L1325 在 catch 内主动 exit
23
+ * (非被外层吞);process.exit 不可被 try 捕获 → 搬迁行为完全等价。
24
+ */
25
+ import { basename, join, resolve, relative, isAbsolute } from 'node:path'
26
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
27
+ import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
28
+ import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot, collectOtherQuickSessionDeclarations } from './shared.js'
29
+ import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
30
+ import { stageRegistry } from '../stages/index.js'
31
+ import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
32
+ import { printQuickAuditReview } from './quick-audit.js'
33
+ import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
34
+ import { getRule } from '../stage-contract-spec.js'
35
+ import { archiveDestDirName } from '../stage-contract.js'
36
+
37
+ /**
38
+ * 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
39
+ * - 必须含至少一个字母(拒绝纯数字 "0"/"7"/"07",避免 scan-projects.json 脏数据)
40
+ * - 长度必须 ≥ 2(拒绝单字符 "a"/"0")
41
+ * @param {string} name - 原始项目名候选
42
+ * @returns {string | null} 合法项目名或 null(拒绝)
43
+ */
44
+ export function sanitizeProjectName(name) {
45
+ if (!name) return null
46
+ const clean = String(name).replace(/[^a-zA-Z0-9_\-.]/g, '').trim()
47
+ if (!clean) return null
48
+ if (!/[a-zA-Z]/.test(clean)) return null // 纯数字/符号拒绝("0"/"7"/"07")
49
+ if (clean.length < 2) return null // 单字符拒绝("a"/"0")
50
+ return clean
51
+ }
52
+ /**
53
+ * 校验从 step 2 解析出的项目列表。
54
+ * 不通过则不落盘 projects/*.yaml、不展开 perProject 步骤。
55
+ *
56
+ * @param {Array<{id: string, path?: string}>} projects - 项目列表(含可选 path)
57
+ * @param {string} sourceRoot - 源码根目录,用于 path 安全校验
58
+ */
59
+ export function validateParsedProjects(projects, sourceRoot) {
60
+ const errors = []
61
+ if (!projects || projects.length === 0) {
62
+ return { ok: false, errors: ['项目列表为空'] }
63
+ }
64
+ if (projects.length > 10) {
65
+ return { ok: false, errors: [`项目数量 ${projects.length} 超过上限 10,疑似误解析`] }
66
+ }
67
+ const safeRoot = resolve(sourceRoot)
68
+ const seen = new Set()
69
+ for (const proj of projects) {
70
+ const id = proj.id || proj
71
+ if (seen.has(id)) {
72
+ errors.push(`重复项目名: ${id}`)
73
+ }
74
+ seen.add(id)
75
+ // slug 合法性:只允许 a-z 0-9 _ - .,长度 2-64
76
+ if (!/^[a-zA-Z][\w\-.]{1,63}$/.test(id)) {
77
+ errors.push(`项目名 "${id}" 不合法(需 slug 格式:字母开头,只含 a-zA-Z0-9_-., 长度 2-64)`)
78
+ }
79
+ // path 安全校验(如果提供了 path)
80
+ if (proj.path) {
81
+ if (proj.path.includes('..')) {
82
+ errors.push(`项目 "${id}" 的 path 包含 .. ,拒绝越界`)
83
+ } else {
84
+ const absPath = resolve(safeRoot, proj.path)
85
+ const rel = relative(safeRoot, absPath)
86
+ if (rel.startsWith('..') || isAbsolute(rel)) {
87
+ errors.push(`项目 "${id}" 的 path "${proj.path}" 解析后超出 source_root`)
88
+ }
89
+ if (!existsSync(absPath)) {
90
+ errors.push(`项目 "${id}" 的 path "${proj.path}" 不存在`)
91
+ }
92
+ }
93
+ }
94
+ }
95
+ if (errors.length > 0) return { ok: false, errors }
96
+ return { ok: true, errors: [] }
97
+ }
98
+ /**
99
+ * 在 changes/archive/ 下查找变更 <changeName> 是否已被归档(archive 脱钩自愈用)。
100
+ *
101
+ * 归档目录名 = 原变更名(直接移入 archive/,不重命名)。
102
+ * 精确匹配 + 要求目录含 plan.md(归档必备产物,防止同名巧合误判)。
103
+ * issue: archive-stage-physical-tracking-desync。
104
+ *
105
+ * @param {string} archiveDir - changes/archive 绝对路径
106
+ * @param {string} changeName - 变更名(currentChange)
107
+ * @returns {string|null} 命中的归档目录绝对路径,无则 null
108
+ */
109
+ export function findAlreadyArchivedDir(archiveDir, changeName) {
110
+ if (!existsSync(archiveDir)) return null
111
+ let entries
112
+ try {
113
+ entries = readdirSync(archiveDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
114
+ } catch {
115
+ return null
116
+ }
117
+ // 精确原名命中
118
+ const exact = entries.find((e) => e === changeName && existsSync(join(archiveDir, e, 'plan.md')))
119
+ if (exact) return join(archiveDir, exact)
120
+ return null
121
+ }
122
+
123
+ /**
124
+ * 归档时清理可能残留的 worktree(execute 自动清理未走到 / 有未 apply 变更被遗弃)。
125
+ * 安全策略:有未 apply 变更时保留 worktree 并警告,避免误删用户未应用的代码。
126
+ * 同时清理该变更的 execute / stage-review runId marker(只写不删的累积物,
127
+ * 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)。
128
+ * 从 archiveChangeDirectory 抽出,供正常归档 + 自愈归档复用。
129
+ */
130
+ export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts = {}) {
131
+ // ── 清理 runId marker:execute(current-execute-run-id-<change>)与 stage-review
132
+ // (current-stage-review-run-id-<stage>-<change>)。marker 只服务 execute→verify→archive
133
+ // 期间,归档后无读者;不删则 .runtime 随变更数无限累积。runtimeRoot 解析同写入侧
134
+ // (resolveRuntimeRoot 锚主仓),避免平台模式清理错位置。
135
+ try {
136
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
137
+ if (existsSync(runtimeRoot)) {
138
+ // stage-review marker 精确匹配(坑 marker-suffix-overmatch):marker 名是
139
+ // current-stage-review-run-id-<stage>-<change>,按 endsWith('-'+change) 后缀匹配会在归档
140
+ // 手工短名变更(如 login)时误删自动日期前缀变更(2026-08-01-login 天然以 '-login' 结尾)
141
+ // 的 marker。stage 集合与写入侧 stageReviewMarkerPath 的调用方一致。
142
+ const REVIEW_STAGES = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
143
+ const markers = readdirSync(runtimeRoot).filter((f) =>
144
+ f === `current-execute-run-id-${archiveChangeName}` ||
145
+ REVIEW_STAGES.some(st => f === `current-stage-review-run-id-${st}-${archiveChangeName}`),
146
+ )
147
+ for (const m of markers) {
148
+ try { unlinkSync(join(runtimeRoot, m)) } catch {}
149
+ }
150
+ if (markers.length > 0) {
151
+ console.log(`🧹 归档清理 ${markers.length} 个 runId marker(execute/stage-review)`)
152
+ }
153
+ }
154
+ } catch (e) {
155
+ console.warn(`⚠️ 归档 runId marker 清理失败(不阻断归档): ${e.message}`)
156
+ }
157
+ try {
158
+ const { WorktreeManager } = await import('../worktree.js')
159
+ const wm = new WorktreeManager({ cwd })
160
+ const meta = wm.getMeta(archiveChangeName)
161
+ if (!meta) {
162
+ // 无 meta 兜底(坑 archive-cleanup-orphan-physical-dir,2026-08-21 实证):meta 已被先行
163
+ // 流程注销(apply 自动 cleanup / doctor 幽灵清理)时此前直接 return——若物理目录因中途
164
+ // 失败残留下就是孤儿(无 meta=无锚定基准,残留即孤儿)。force 清理(幂等:什么都不存在
165
+ // 时 cleanup 返回 skipped 零副作用)。
166
+ const orphan = wm.cleanup(archiveChangeName, { force: true })
167
+ if (orphan.result === 'cleaned' || orphan.result === 'force-cleaned' || orphan.result === 'partial') {
168
+ console.log(`🧹 归档清理孤儿 worktree 残留(meta 已注销): ${archiveChangeName}${orphan.residual?.length ? '(残留: ' + orphan.residual.join('; ') + ')' : ''}`)
169
+ }
170
+ return
171
+ }
172
+ const check = meta.mode !== 'in-place-fallback' ? wm.hasUnappliedChanges(archiveChangeName) : { hasChanges: false }
173
+ if (check.hasChanges) {
174
+ console.warn(`⚠️ 归档时 worktree 仍有 ${check.changedFiles.length} 个未 apply 变更,保留 worktree`)
175
+ console.warn(` 确认不需要后手动清理: sillyspec worktree cleanup ${archiveChangeName} --force`)
176
+ return
177
+ }
178
+ const cleanResult = wm.cleanup(archiveChangeName)
179
+ if (cleanResult.residual?.length > 0) {
180
+ console.warn(`⚠️ 归档 worktree 清理残留: ${cleanResult.residual.join('; ')}`)
181
+ console.warn(` 手动处理: sillyspec worktree cleanup ${archiveChangeName} --force`)
182
+ }
183
+ } catch (e) {
184
+ console.warn(`⚠️ 归档 worktree 清理失败(不阻断归档): ${e.message}`)
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 提取 module-impact.md「更新结果」表中的未清文档同步死信行(债单 D-5)。
190
+ *
191
+ * 只在「## 更新结果」段内扫描表格行(| 分隔),状态判定取**末列** trim 后精确匹配
192
+ * pending / 待办 / 未同步 / not-done(大小写不敏感)。不做全文 grep——
193
+ * sync_manual_get_pending / pending_review / pending-leases 等代码标识符出现在
194
+ * 矩阵摘要列是合法内容,不能误报(实证:archive 全量扫描唯一死信形态是末列 pending)。
195
+ *
196
+ * 纯函数:无 fs/git 调用,export 供 test 直接 import。
197
+ * @param {string} content module-impact.md 全文(CRLF/LF 均容)
198
+ * @returns {string[]} 死信行原文(去首尾空白),空数组 = 无死信
199
+ */
200
+ export function extractPendingDocSyncRows(content) {
201
+ if (!content || typeof content !== 'string') return []
202
+ const normalized = content.replace(/\r\n/g, '\n')
203
+ const sectionStart = normalized.search(/^#{2,3}\s*更新结果\s*$/m)
204
+ if (sectionStart === -1) return []
205
+ // 跳过标题行本身(matchFirstLine 吃掉标题),再找下一个章节边界
206
+ const titleMatch = normalized.slice(sectionStart).match(/^#{2,3}\s*更新结果\s*$/m)
207
+ const bodyStart = sectionStart + titleMatch[0].length
208
+ const afterTitle = normalized.slice(bodyStart)
209
+ const nextSection = afterTitle.search(/^#{1,3}\s/m)
210
+ const section = nextSection === -1 ? afterTitle : afterTitle.slice(0, nextSection)
211
+ const PENDING_STATUSES = new Set(['pending', '待办', '未同步', 'not-done', 'todo'])
212
+ const rows = []
213
+ for (const line of section.split('\n')) {
214
+ const trimmed = line.trim()
215
+ if (!trimmed.startsWith('|')) continue
216
+ const cells = trimmed.split('|').map(c => c.trim())
217
+ // split 首尾产生空串(行首行尾的 |),真实单元格在中间;末列 = 最后一个非空单元格之后
218
+ // 表格行结构 | 模块文档 | 操作 | 状态 | → cells = ['', '模块文档', '操作', '状态', '']
219
+ const lastCell = cells[cells.length - 1]
220
+ if (lastCell === '' && cells.length >= 2) cells.pop()
221
+ const statusCell = cells[cells.length - 1]
222
+ if (statusCell && PENDING_STATUSES.has(statusCell.toLowerCase())) rows.push(trimmed)
223
+ }
224
+ return rows
225
+ }
226
+
227
+ /**
228
+ * 提取 module-impact.md「更新结果」表中的已完成(done)行声明的目标文档路径(债单 D-4 窄口径)。
229
+ *
230
+ * done 行首列是目标标识:`modules/<id>.md`(模块卡片)、`_module-map.yaml`(映射索引)、
231
+ * `.sillyspec/docs/<project>/modules/<f>.md`(含仓库前缀的全路径)等。本函数提取首列里
232
+ * 能解析出的文档路径 token,供调用方校验文件存在性——声明 done 但目标文件不存在 = 假申报。
233
+ *
234
+ * 提取规则(保守,宁可漏报不误报):
235
+ * - 首列以反引号包裹或裸写的 `.md`/`.yaml` 结尾路径 token(含路径分隔符 /)
236
+ * - `modules/<id>.md` 相对写法解析为 `<specBase>/docs/<project>/modules/<id>.md`——但 project
237
+ * 归属需读 _module-map,此处只返回原样 token,路径解析留给调用方(有 specBase 上下文)
238
+ * - `_module-map.yaml` 裸名跳过(存在性由 map 本身保证,且各项目都有)
239
+ *
240
+ * 纯函数:无 fs/git 调用,export 供 test 直接 import。
241
+ * @param {string} content module-impact.md 全文
242
+ * @returns {string[]} done 行声明的文档路径 token(去重)
243
+ */
244
+ export function extractDoneDocTargets(content) {
245
+ if (!content || typeof content !== 'string') return []
246
+ const normalized = content.replace(/\r\n/g, '\n')
247
+ const sectionStart = normalized.search(/^#{2,3}\s*更新结果\s*$/m)
248
+ if (sectionStart === -1) return []
249
+ const titleMatch = normalized.slice(sectionStart).match(/^#{2,3}\s*更新结果\s*$/m)
250
+ const bodyStart = sectionStart + titleMatch[0].length
251
+ const afterTitle = normalized.slice(bodyStart)
252
+ const nextSection = afterTitle.search(/^#{1,3}\s/m)
253
+ const section = nextSection === -1 ? afterTitle : afterTitle.slice(0, nextSection)
254
+ const targets = new Set()
255
+ for (const line of section.split('\n')) {
256
+ const trimmed = line.trim()
257
+ if (!trimmed.startsWith('|')) continue
258
+ const cells = trimmed.split('|').map(c => c.trim())
259
+ if (cells.length >= 2) cells.pop() // 行尾空 cell
260
+ const statusCell = cells[cells.length - 1]
261
+ if (!statusCell || statusCell.toLowerCase() !== 'done') continue
262
+ // 首个非空 cell(跳过 split 产生的行首空串)
263
+ const firstCell = cells.slice(1).find(c => c !== '') || ''
264
+ // 从首列提取 .md/.yaml 结尾且含 / 的路径 token(modules/x.md 或 a/b/c.yaml);
265
+ // 反引号包裹优先,裸写次之。_module-map.yaml 无 / 跳过(见 docstring)。
266
+ const m = firstCell.match(/`([^`]*\.(?:md|yaml))`/) || firstCell.match(/(^|\s|()([\w./-]+\/[\w./-]*\.(?:md|yaml))/)
267
+ if (m) {
268
+ const token = (m[2] !== undefined && m[2] !== null && m[1] !== '`') ? m[2] : m[1]
269
+ if (token && !token.endsWith('_module-map.yaml')) targets.add(token)
270
+ }
271
+ }
272
+ return [...targets]
273
+ }
274
+
275
+ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts = {}) {
276
+ const archiveChangeName = progress.currentChange
277
+ if (!archiveChangeName) {
278
+ console.error('❌ 归档失败:未找到当前变更名(currentChange)')
279
+ process.exit(1)
280
+ }
281
+ const changesDir = join(specBase, 'changes')
282
+ const archiveDir = join(changesDir, 'archive')
283
+ const srcDir = join(changesDir, archiveChangeName)
284
+ const date = new Date().toISOString().slice(0, 10)
285
+ const destName = archiveDestDirName(date, archiveChangeName)
286
+ const destDir = join(archiveDir, destName)
287
+
288
+ if (!existsSync(srcDir)) {
289
+ // 幂等自愈(issue archive-stage-physical-tracking-desync):源目录已不存在,但变更可能已被
290
+ // 手动 / 部分流程移到 changes/archive/ 并 commit,而 --done --confirm 从未正式跑完 → 进度 DB
291
+ // 卡 archive 阶段、active 列表仍列此 change。检测到已归档则回填进度(unregisterChange)并
292
+ // 成功返回,让收尾流程把 archive 阶段标完成,而非 exit(1) 死路(source 已移走无法重跑 move)。
293
+ const alreadyArchivedDir = findAlreadyArchivedDir(archiveDir, archiveChangeName)
294
+ if (alreadyArchivedDir) {
295
+ console.log(`ℹ️ 源目录不存在但变更已在 archive/(${basename(alreadyArchivedDir)}),判定已归档,自愈进度 DB`)
296
+ // 终态一致化(坑 manual-archive-desync-status-only):手动搬目录绕过标准归档后,本自愈路径
297
+ // 不能只翻 status——同时收尾 archive 阶段(steps/stages/current_stage),否则推送平台的终态
298
+ // 是「已归档 + 归档 0/5 + 停在 execute」的矛盾体,详情页渲染成「进度丢失」
299
+ pm.unregisterChange(cwd, archiveChangeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
300
+ await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
301
+ console.log(`📦 已自愈归档:${archiveChangeName} → archive/${basename(alreadyArchivedDir)}/`)
302
+ return alreadyArchivedDir
303
+ }
304
+ console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
305
+ console.error(` 且 changes/archive/ 下未找到该变更的归档目录。若已手动归档请核对目录名;否则先补全变更产物。`)
306
+ process.exit(1)
307
+ }
308
+ // 移动前硬校验:变更包必须含 plan.md,否则不该归档。
309
+ // 在移动前阻断(而非移动后),目录尚未动,用户可直接修复后重试。
310
+ if (!existsSync(join(srcDir, 'plan.md'))) {
311
+ console.error(`❌ 归档失败:变更目录缺少 plan.md(${srcDir})`)
312
+ console.error(` plan.md 是归档的必需产物。请先补全 plan 阶段产出再归档。`)
313
+ process.exit(1)
314
+ }
315
+ // 移动前硬校验(债单 D-5):module-impact.md「更新结果」表存在 pending 死信 → 阻断。
316
+ // 修复场景:perf-remediation 类变更把文档同步显式推给 archive(「(execute 完成后由 archive
317
+ // 阶段同步)| 待办 | pending」),archive 又没做 → 带 pending 归档且 verify 全 PASS,死信无人回填。
318
+ // 只查「## 更新结果」段内表格行的状态列(末列)精确匹配 pending/待办/未同步——不做全文 grep,
319
+ // 防 sync_manual_get_pending / pending_review 等代码标识符误报。
320
+ const impactPath = join(srcDir, 'module-impact.md')
321
+ if (existsSync(impactPath)) {
322
+ const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
323
+ if (pendingRows.length > 0) {
324
+ console.error(`❌ 归档失败:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
325
+ for (const row of pendingRows) console.error(` - ${row}`)
326
+ console.error(` 这些文档同步项从未落地。请先完成同步并回填状态为 done/skipped(说明原因),再归档。`)
327
+ process.exit(1)
328
+ }
329
+ }
330
+ if (existsSync(destDir)) {
331
+ console.error(`❌ 归档失败:目标目录已存在 ${destDir}`)
332
+ process.exit(1)
333
+ }
334
+ mkdirSync(archiveDir, { recursive: true })
335
+ try {
336
+ renameSyncRetry(srcDir, destDir)
337
+ } catch (e) {
338
+ console.error(`❌ 归档失败:移动变更目录时出错(${e.message})`)
339
+ console.error(` 常见原因:变更目录内文件被 IDE / 杀毒 / 索引占用,已重试 5 次仍失败。请关闭相关程序后重试。`)
340
+ process.exit(1)
341
+ }
342
+
343
+ if (!existsSync(destDir) || existsSync(srcDir)) {
344
+ console.error('❌ 归档校验失败:移动操作异常')
345
+ process.exit(1)
346
+ }
347
+
348
+ // 正常路径同样走终态一致化(坑 manual-archive-desync-status-only):标准 --done --confirm 流程
349
+ // 中 completeStep 随后也会逐项完成,此处先收尾是幂等写同值——保证任何走出本函数的归档终态一致
350
+ pm.unregisterChange(cwd, archiveChangeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
351
+
352
+ // CLI 下沉 git add(坑4,FR-04):确定性暂存归档目录 + 模块文档,不靠 step5 prompt 驱动。
353
+ // step5 prompt 的 git add 保留作幂等兜底;POSIX 路径跨平台(git 接受正斜杠)。
354
+ // safeGit 内部已 try-catch(返回 {value,error} 不抛),外层 try 兜底防御;失败不阻断归档
355
+ // (目录已移动 + change 已注销),由 step5 prompt git add + agent git status 核对兜底。
356
+ try {
357
+ safeGit(cwd, ['add', '--', '.sillyspec/changes/archive/'])
358
+ safeGit(cwd, ['add', '--', '.sillyspec/docs/'])
359
+ } catch {}
360
+
361
+ // ── 他者半归档残留探测(坑 archive-other-residual-rename,2026-08-21 实证)──
362
+ // 并行变更的手动归档把 R 残留(源目录 rename 目标行)留在暂存区;本变更归档提交时
363
+ // git status 显示它们,agent 会误判「还没提交完 / 要做第二次提交」。区分:
364
+ // 本变更:未暂存的源侧移动(` D changes/<me>/...` + 未跟踪 archive/<me>/...)→ 补暂存
365
+ // 让归档成单次原子提交;
366
+ // 他者:已暂存的 rename(`R changes/<他人>/... -> changes/archive/<他人>/...`)→ 只 warn
367
+ // 提示归属(不 stage 不动——别人的归别人的)。
368
+ try {
369
+ const raw = gitQuiet(cwd, ['status', '--porcelain'], { trim: false, timeout: 30000 })
370
+ if (raw && raw.trim()) {
371
+ const changesPrefix = '.sillyspec/changes/'
372
+ const minePaths = []
373
+ const othersResidual = []
374
+ for (const line of raw.split('\n')) {
375
+ if (!line || line.length < 4) continue
376
+ const x = line[0], y = line[1]
377
+ const body = line.slice(3).trim()
378
+ const arrow = body.indexOf(' -> ')
379
+ const src = arrow !== -1 ? body.slice(0, arrow).replace(/^"|"$/g, '') : null
380
+ const dst = (arrow !== -1 ? body.slice(arrow + 4) : body).replace(/^"|"$/g, '')
381
+ if (x === 'R' && y === ' ' && arrow !== -1 && dst.startsWith(changesPrefix + 'archive/')) {
382
+ // 已暂存 rename → 半归档残留;按目标目录名归属他者变更
383
+ const m = /^\.sillyspec\/changes\/archive\/([^/]+)\//.exec(dst)
384
+ const owner = m ? m[1] : '?'
385
+ if (owner !== archiveChangeName) othersResidual.push(owner)
386
+ } else if (x === ' ' && (y === 'D' || y === 'A' || y === 'M')) {
387
+ // 未暂存的源侧移动:源目录 D / 新位置 A——指向本变更的补暂存
388
+ const p = dst || src
389
+ if (p && (p.startsWith(changesPrefix + archiveChangeName + '/')
390
+ || p.startsWith(changesPrefix + 'archive/' + archiveChangeName + '/'))) {
391
+ minePaths.push(p)
392
+ }
393
+ }
394
+ }
395
+ if (minePaths.length > 0) {
396
+ try { safeGit(cwd, ['add', '-A', '--', changesPrefix]) } catch {}
397
+ console.log(`🧾 已补暂存本变更归档的源侧移动(${minePaths.length} 项,归档成单次原子提交)`)
398
+ }
399
+ if (othersResidual.length > 0) {
400
+ const owners = [...new Set(othersResidual)].filter(Boolean)
401
+ console.warn(`⚠️ 检测到「他者半归档」残留(暂存区存在其他变更的 rename 记录):${owners.join('、')}`)
402
+ console.warn(` 这些是别的变更此前手动归档留下的暂存项,不属于本次归档——git status 里看到它们是正常的,`)
403
+ console.warn(` 本变更归档已完成,无需为其做第二次提交;如需清理走它们自己的收尾(或 git restore --staged 后核对)。`)
404
+ }
405
+ }
406
+ } catch { /* 探测失败不阻断归档(advisory) */ }
407
+
408
+ // 归档时清理可能残留的 worktree(自愈路径也复用,见上方 srcDir 缺失分支)。
409
+ await archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts)
410
+
411
+ console.log(`📦 已归档:${archiveChangeName} → archive/${destName}/`)
412
+ return destDir
413
+ }
414
+ /**
415
+ * archive 阶段「确认归档」步骤的收尾处理器(从 completeStep 抽出,行为保持)。
416
+ *
417
+ * 两件事:
418
+ * 1. --confirm 门控:缺 --confirm → 回退 step 状态、提示、返回 early-return 对象(completeStep 透传)
419
+ * 2. --confirm 通过 → archiveChangeDirectory 移动变更目录 + 推荐文档(design.md / module-impact.md)校验
420
+ * (contracts.archive.validators 为空,两个 validator 生效窗口互斥;plan.md 已在移动前硬校验阻断)
421
+ *
422
+ * @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
423
+ */
424
+ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts = {} }) {
425
+ if (stageName !== 'archive' || steps[currentIdx]?.name !== '确认归档') return null
426
+ if (!confirm) {
427
+ steps[currentIdx].status = 'pending'
428
+ steps[currentIdx].completedAt = null
429
+ if (outputText) steps[currentIdx].output = null
430
+ pm._write(cwd, progress, changeName)
431
+ console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
432
+ return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
433
+ }
434
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):archiveChangeDirectory 改主仓共享状态(目录
435
+ // rename + 共享 index 的 git add + marker 删除 + worktree 清理),与并行会话的 apply/cleanup
436
+ // 互踩。exit 钩子兜底:其内部 guard 失败 exit(1) 时锁也会被清(见 withMainRepoLock)。
437
+ const { withMainRepoLock } = await import('../worktree-apply.js')
438
+ const archivedDir = await withMainRepoLock(cwd, changeName, 'archive-finalize', () => archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts))
439
+ if (archivedDir && existsSync(archivedDir)) {
440
+ // 内存快照同步(坑 archive-progress-show-stale,2026-08-21 实证):archiveChangeDirectory 内
441
+ // unregisterChange 已在 DB 写 current_stage='archive'(终态一致化),但本进程 progress 是命令
442
+ // 开始时读的旧快照——不同步的话,completeStep 后续 _write 会用旧值(如停在 verify)把
443
+ // DB 覆盖回去,progress show 就一直显示归档前的阶段
444
+ progress.currentStage = 'archive'
445
+ const recommendedDocs = ['design.md', 'module-impact.md']
446
+ const missingRecommended = recommendedDocs.filter(d => !existsSync(join(archivedDir, d)))
447
+ if (missingRecommended.length > 0) {
448
+ console.warn(`\n⚠️ 归档校验警告:归档目录缺少推荐文档`)
449
+ for (const d of missingRecommended) console.warn(` - ${d}(${archivedDir})`)
450
+ } else {
451
+ console.log(`\n✅ 归档校验通过:核心文档齐全`)
452
+ }
453
+ // D-4 窄口径:更新结果表 done 行声明的目标文档存在性对账(warning 不阻断——
454
+ // 目标路径写法多样(modules/x.md 相对/全路径)+ 语义对账推 sillyhub,此处只抓
455
+ // 能确定性解析且确实不存在的假申报)。
456
+ const impactContent = existsSync(join(archivedDir, 'module-impact.md'))
457
+ ? readFileSync(join(archivedDir, 'module-impact.md'), 'utf8') : ''
458
+ const doneTargets = extractDoneDocTargets(impactContent)
459
+ if (doneTargets.length > 0) {
460
+ const missing = doneTargets.filter(t => {
461
+ // 相对 specBase 解析:modules/<x> → docs/<project>/modules/<x> 由 _module-map 归属,
462
+ // CLI 不猜 project——只对全路径(.sillyspec/docs/ 开头或含 3+ 段路径)做存在性判定
463
+ if (!t.includes('/')) return false
464
+ const abs = isAbsolute(t) ? t : resolve(cwd, t)
465
+ return !existsSync(abs)
466
+ })
467
+ if (missing.length > 0) {
468
+ console.warn(`\n⚠️ 归档校验警告:module-impact「更新结果」声明 done 的文档路径不存在(假申报嫌疑)`)
469
+ for (const t of missing) console.warn(` - ${t}`)
470
+ console.warn(` 相对写法(modules/<id>.md)不在此校验范围;全路径声明但文件不存在请核实。`)
471
+ }
472
+ }
473
+ }
474
+ return null
475
+ }
476
+ /**
477
+ * plan 阶段「generate_plan」步骤完成后,动态插入任务蓝图(coordinator)+ postcheck 步骤
478
+ * (从 completeStep 抽出,行为保持)。使用稳定 id 匹配,不依赖中文标题。
479
+ *
480
+ * plan.md 已含任务时,buildPlanSteps 返回 [fixedPrefix(classify/generate_plan/review_plan),
481
+ * coordinator, postcheck];本函数把 generate_plan 之后的 coordinator+postcheck 插到当前步后。
482
+ */
483
+ export async function handlePlanGeneratePlanStep({ stageName, steps, currentIdx, defStepsForCurrent, cwd, progress }) {
484
+ if (stageName !== 'plan') return
485
+ const currentStepDef = defStepsForCurrent?.[currentIdx]
486
+ const currentStepEntry = steps[currentIdx]
487
+ const stepId = currentStepDef?.id || currentStepEntry?.id || currentStepEntry?._stepId
488
+ if (stepId !== 'generate_plan') return
489
+ const changeDir = resolveChangeDir(cwd, progress)
490
+ if (!changeDir) return
491
+ const planFile = join(changeDir, 'plan.md')
492
+ if (!existsSync(planFile)) return
493
+ const planContent = readFileSync(planFile, 'utf8')
494
+ const { buildPlanSteps, fixedPrefix, fixedSuffix } = await import('../stages/plan.js')
495
+ const fullSteps = buildPlanSteps(changeDir, planContent)
496
+ const prefixLen = fixedPrefix.length
497
+ const suffixLen = fixedSuffix.length
498
+ // 新结构:[...fixedPrefix, coordinatorStep?, postcheckStep?];fixedSuffix 为空,coordinator+postcheck 都在 prefix 之后
499
+ const coordinatorSteps = fullSteps.slice(prefixLen, suffixLen > 0 ? -suffixLen : undefined)
500
+ if (coordinatorSteps.length === 0) return
501
+ for (let i = 0; i < coordinatorSteps.length; i++) {
502
+ const stepDef = coordinatorSteps[i]
503
+ const stepEntry = {
504
+ id: stepDef.id,
505
+ name: stepDef.name,
506
+ status: 'pending',
507
+ prompt: stepDef.prompt || '',
508
+ outputHint: stepDef.outputHint,
509
+ optional: stepDef.optional
510
+ }
511
+ // 传递 noAI / _cliAction 属性
512
+ if (stepDef.noAI) stepEntry.noAI = true
513
+ if (stepDef._cliAction) stepEntry._cliAction = stepDef._cliAction
514
+ steps.splice(currentIdx + 1 + i, 0, stepEntry)
515
+ }
516
+ console.log(` 📝 已动态插入 ${coordinatorSteps.length} 个步骤(${coordinatorSteps.map(s => s.name).join(', ')})`)
517
+ }
518
+ /**
519
+ * scan 阶段 step 2「构建扫描项目列表」完成后,按项目展开 perProject 步骤(从 completeStep 抽出,行为保持)。
520
+ *
521
+ * 只接受结构化输出(scan_projects YAML block 或 BEGIN_PROJECT_LIST 标记块),校验通过后
522
+ * 自动注册 projects/<id>.yaml + 写 scan-projects.json + 把 perProject 步骤按项目展开。
523
+ * 不展开 completeStep 提前 return(失败只记 validationError,由 completeStep 继续推进下一步)。
524
+ */
525
+ export async function handleScanProjectListStep({ stageName, steps, currentIdx, outputText, stageData, specBase, cwd, platformOpts }) {
526
+ if (stageName !== 'scan' || steps[currentIdx]?.name !== '构建扫描项目列表') return
527
+ // 解析项目列表:只接受结构化输出(YAML block 或 BEGIN_PROJECT_LIST 标记)
528
+ // 不再从自由文本猜测项目名——自由文本列表的误解析会导致垃圾项目落盘
529
+ let parsedProjects = [] // Array<{id, path?}>
530
+ let parsedFromStructuredOutput = false
531
+ if (outputText) {
532
+ // 格式 A: YAML block — 匹配 scan_projects: 下所有 - id: xxx 条目(含多行属性)
533
+ const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
534
+ if (yamlBlock) {
535
+ const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
536
+ for (const m of entries) {
537
+ const id = sanitizeProjectName(m[1])
538
+ if (!id) continue
539
+ // 提取可选 path 字段
540
+ const pathMatch = m[0].match(/path:\s*(\S+)/)
541
+ const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
542
+ parsedProjects.push(entry)
543
+ }
544
+ parsedFromStructuredOutput = parsedProjects.length > 0
545
+ }
546
+ // 格式 B: BEGIN_PROJECT_LIST ... END_PROJECT_LIST 标记块
547
+ if (!parsedFromStructuredOutput) {
548
+ const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
549
+ if (blockMatch) {
550
+ const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
551
+ parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
552
+ parsedFromStructuredOutput = parsedProjects.length > 0
553
+ }
554
+ }
555
+ }
556
+
557
+ const projectNames = parsedProjects.map(p => p.id)
558
+
559
+ if (parsedFromStructuredOutput) {
560
+ stageData.scanMeta = stageData.scanMeta || {}
561
+ stageData.scanMeta.projectListParsed = true
562
+ } else {
563
+ // 结构化输出未解析到 → 回退读取已有 projects/*.yaml
564
+ // 读取时也校验:path 不存在的 yaml 视为垃圾,直接跳过
565
+ console.warn('⚠️ step 2 未输出结构化项目列表,回退扫描已注册项目')
566
+ stageData.scanMeta = stageData.scanMeta || {}
567
+ stageData.scanMeta.projectListParsed = false
568
+ const projectsDir = join(specBase, 'projects')
569
+ if (existsSync(projectsDir)) {
570
+ const yamlFiles = readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
571
+ const fallbackProjects = []
572
+ const fallbackSkipped = []
573
+ for (const yf of yamlFiles) {
574
+ const pName = yf.replace(/\.yaml$/, '')
575
+ const yamlContent = readFileSync(join(projectsDir, yf), 'utf8')
576
+ const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
577
+ const pPath = pathMatch ? pathMatch[1].trim() : pName
578
+ // 校验 path 是否存在且在 source_root 内。
579
+ // 用 relative 判越界(与 validateParsedProjects 同口径):startsWith 前缀比较
580
+ // 无分隔符,C:\repo 会放行兄弟目录 C:\repository2(体检 BUG-13)
581
+ const absPath = resolve(cwd, pPath)
582
+ const rel = relative(resolve(cwd), absPath)
583
+ if (rel.startsWith('..') || isAbsolute(rel)) {
584
+ fallbackSkipped.push(`${pName} (path 越界: ${pPath})`)
585
+ continue
586
+ }
587
+ if (!existsSync(absPath)) {
588
+ fallbackSkipped.push(`${pName} (path 不存在: ${pPath})`)
589
+ continue
590
+ }
591
+ fallbackProjects.push({ id: pName, path: pPath })
592
+ }
593
+ if (fallbackSkipped.length > 0) {
594
+ console.warn(`⚠️ 跳过 ${fallbackSkipped.length} 个垃圾/过期项目配置:${fallbackSkipped.join(', ')}`)
595
+ console.warn(' 建议清理 projects/ 下的无效 yaml 文件')
596
+ }
597
+ parsedProjects = fallbackProjects
598
+ projectNames.length = 0
599
+ projectNames.push(...fallbackProjects.map(p => p.id))
600
+ }
601
+ if (parsedProjects.length === 0) {
602
+ // 无结构化输出 + 无合法已有项目 → step 2 失败
603
+ console.error('❌ step 2 未输出结构化项目列表,且 projects/ 下无合法项目配置')
604
+ console.error(' 请在 --output 中输出 scan_projects YAML block 或 BEGIN_PROJECT_LIST 标记块')
605
+ steps[currentIdx].validationError = '未输出结构化项目列表且无合法 fallback'
606
+ // 不展开 perProject 步骤,直接跳到下一步
607
+ }
608
+ }
609
+
610
+ // 校验解析出的项目列表(原子守卫:不通过就不落盘)
611
+ const validation = validateParsedProjects(parsedProjects, cwd)
612
+ if (!validation.ok) {
613
+ console.error(`❌ 项目列表校验失败: ${validation.errors.join('; ')}`)
614
+ console.error(' step 2 完成,但不展开 perProject 步骤。请检查 --output 中的项目列表。')
615
+ steps[currentIdx].validationError = validation.errors.join('; ')
616
+ }
617
+
618
+ // 自动注册 + 保存 runtime + 展开 perProject 步骤(仅在校验通过时)
619
+ const projectsDir = join(specBase, 'projects')
620
+ if (validation.ok) {
621
+ for (const proj of parsedProjects) {
622
+ const pName = proj.id
623
+ const projYaml = join(projectsDir, `${pName}.yaml`)
624
+ if (!existsSync(projYaml)) {
625
+ mkdirSync(projectsDir, { recursive: true })
626
+ const candidates = [
627
+ join(cwd, pName),
628
+ join(cwd, 'backend', pName),
629
+ join(cwd, 'packages', pName),
630
+ join(cwd, 'apps', pName),
631
+ join(cwd, 'services', pName),
632
+ ]
633
+ const detected = candidates.find(c => existsSync(c))
634
+ const regPath = detected || join(cwd, pName)
635
+ writeFileSync(projYaml, `name: ${pName}\npath: ${regPath}\nstatus: active\n`)
636
+ console.log(` 📝 自动注册子项目: ${pName} → ${regPath}`)
637
+ }
638
+ }
639
+
640
+ // 保存 runtime 状态
641
+ const scanStatePath = join(specBase, '.runtime', 'scan-projects.json')
642
+ mkdirSync(join(specBase, '.runtime'), { recursive: true })
643
+ let scanState = { projects: projectNames, expanded: false }
644
+ if (existsSync(scanStatePath)) {
645
+ try { scanState = JSON.parse(readFileSync(scanStatePath, 'utf8')) } catch {}
646
+ }
647
+
648
+ // 收集当前步骤之后所有 perProject 步骤
649
+ const stageDef = stageRegistry[stageName]
650
+ const allSteps = stageDef?.steps || []
651
+ const perProjectSteps = allSteps.filter(s => s.perProject)
652
+
653
+ // 防重复展开
654
+ const alreadyExpanded = scanState.expanded || steps.some(s => s.name?.match(/\[.+\]\s*$/))
655
+ if (!alreadyExpanded && perProjectSteps.length > 0) {
656
+ // 找到当前步骤(step 2)在动态 steps 中的位置
657
+ const insertBase = currentIdx + 1
658
+ let insertPos = insertBase
659
+ for (const pName of projectNames) {
660
+ // 读取项目配置获取 projectRoot
661
+ const projYaml = join(specBase, 'projects', `${pName}.yaml`)
662
+ let projectRoot = '.'
663
+ if (existsSync(projYaml)) {
664
+ const yamlContent = readFileSync(projYaml, 'utf8')
665
+ const pathMatch = yamlContent.match(/^path:\s*(.+)/m)
666
+ if (pathMatch) projectRoot = pathMatch[1].trim()
667
+ }
668
+ const docOutputDir = platformOpts.specRoot ? `${specBase}/docs/${pName}` : `.sillyspec/docs/${pName}`
669
+ const contextPrefix = `\n---\n## 当前项目\n- **项目名**: ${pName}\n- **项目路径**: ${projectRoot}\n- **文档输出**: ${docOutputDir}\n\n⚠️ 本步骤只处理上面这个项目,不要处理其他项目。\n---\n\n`
670
+
671
+ for (const ppStep of perProjectSteps) {
672
+ steps.splice(insertPos, 0, {
673
+ name: `${ppStep.name} [${pName}]`,
674
+ project: pName,
675
+ status: 'pending',
676
+ prompt: contextPrefix + ppStep.prompt,
677
+ outputHint: ppStep.outputHint,
678
+ optional: ppStep.optional
679
+ })
680
+ insertPos++
681
+ }
682
+ }
683
+ // 移除原始的 perProject 步骤(未展开的版本)
684
+ for (let i = steps.length - 1; i >= 0; i--) {
685
+ if (steps[i].perProject && !steps[i].name?.includes('[')) {
686
+ steps.splice(i, 1)
687
+ }
688
+ }
689
+ console.log(` 📝 已按项目展开 ${perProjectSteps.length} 个步骤 × ${projectNames.length} 个项目 = ${perProjectSteps.length * projectNames.length} 个项目步骤`)
690
+ console.log(` 📁 扫描项目:${projectNames.join(', ')}`)
691
+ // 标记已展开,防止 resume 重复插入
692
+ scanState.expanded = true
693
+ writeFileSync(scanStatePath, JSON.stringify(scanState))
694
+ } // end !alreadyExpanded
695
+ } // end validation.ok
696
+ }
697
+ /**
698
+ * Workflow post_check(W6 Step6 从 completeStep 内联块抽出):
699
+ * - scan「深度扫描」完成 → 跑 scan-docs workflow postcheck,失败阻断推进(early-return)
700
+ * - archive「extract-module-impact」完成 → 跑 archive-impact workflow postcheck(impact-analyzer 结果)
701
+ * 返回 early-return 对象({stageCompleted:false,...})由 completeStep 透传;null = 放行。
702
+ *
703
+ * ctx 字段:stageName/steps/currentIdx/cwd/specBase/progress/platformOpts/changeName(completeStep 局部)。
704
+ * 辅助函数直接 import:basename/join/existsSync/readdirSync(顶部静态);loadWorkflow/runPostCheck/
705
+ * formatCheckReport/saveWorkflowRun 动态 import ../workflow.js(真环依赖保留动态)。
706
+ *
707
+ *搬迁清理:删除死代码 `typeof change !== 'undefined'`(completeStep 作用域无 change 变量,恒 null)。
708
+ */
709
+ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cwd, specBase, progress, platformOpts, changeName }) {
710
+ // Workflow post_check:scan 深度扫描完成后自动检查产物
711
+ if (stageName === 'scan' && steps[currentIdx]?.name?.includes('深度扫描')) {
712
+ try {
713
+ const { loadWorkflow, runPostCheck, formatCheckReport, saveWorkflowRun } = await import('../workflow.js')
714
+ const wf = loadWorkflow(cwd, 'scan-docs')
715
+ if (wf) {
716
+ // 确定当前项目(优先级链):
717
+ // progress.project (dbProjectName,平台模式真实项目名,与 outputStep 占位符渲染对齐)
718
+ // > change?.project (变更对象的项目字段,平台模式 change 创建时传入)
719
+ // > steps[idx].project (perProject 展开标记,兼容旧模式)
720
+ // > steps[idx].name 正则提取 [xxx] 后缀
721
+ // > null(回退检查所有项目)
722
+ // task-05 修复:日志显示项目名变 frontend 是 perProject 误展开 bug,
723
+ // 用 progress.project(与 outputStep 占位符渲染路径一致)修正 myaaa/frontend 分裂。
724
+ const currentProjectName = progress.project
725
+ || steps[currentIdx].project
726
+ || (steps[currentIdx].name.match(/\[([^\]]+)\]\s*$/) || [])[1]
727
+ || null
728
+
729
+ // 确定要检查的项目列表
730
+ let projectsToCheck = []
731
+ if (currentProjectName) {
732
+ // 按项目展开模式:只检查当前项目
733
+ projectsToCheck = [currentProjectName]
734
+ } else {
735
+ // 兼容旧模式(未展开):检查所有项目
736
+ const projectsDir = join(specBase, 'projects')
737
+ const projectFiles = existsSync(projectsDir)
738
+ ? readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))
739
+ : []
740
+ projectsToCheck = projectFiles.map(f => f.replace(/\.yaml$/, ''))
741
+ }
742
+
743
+ let anyFailed = false
744
+ for (const pName of projectsToCheck) {
745
+ const result = runPostCheck(wf, cwd, pName, {}, specBase)
746
+ const report = formatCheckReport(result)
747
+ console.log(report)
748
+ if (result.status === 'fail') {
749
+ anyFailed = true
750
+ // retry_prompts 由 _checkWorkflow 自动生成
751
+ for (const rp of (result.retry_prompts || [])) {
752
+ console.log(`\n🔄 重试提示(项目 ${pName}):\n`)
753
+ console.log(rp.prompt)
754
+ }
755
+ }
756
+ const saved = saveWorkflowRun(result, {
757
+ cwd,
758
+ source: 'run.js',
759
+ stage: 'scan',
760
+ step: steps[currentIdx]?.name,
761
+ ...(platformOpts.runtimeRoot ? { runtimeRoot: platformOpts.runtimeRoot } : {}),
762
+ ...(platformOpts.scanRunId ? { scanRunId: platformOpts.scanRunId } : {})
763
+ })
764
+ if (saved) console.log(`📁 结果已归档:${saved}`)
765
+ }
766
+ if (anyFailed) {
767
+ console.log(`\n⚠️ 存在检查失败项,请按上面的重试提示修复后再继续。`)
768
+ // task-07: 阻断推进(与 task-06 平台模式 scan-postcheck 失败分支 return 结构对齐)
769
+ // scan 深度扫描产物校验未通过时,不允许 clean success / 进入下一 step,
770
+ // 让上层走"完成但不推进"分支,--done 被拒。
771
+ return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
772
+ }
773
+ }
774
+ } catch (e) {
775
+ console.warn(`⚠️ workflow 检查跳过:${e.message}`)
776
+ }
777
+ }
778
+
779
+ // Workflow post_check:archive extract-module-impact 完成后检查产物
780
+ if (stageName === 'archive' && steps[currentIdx]?.name?.includes('extract-module-impact')) {
781
+ try {
782
+ const { loadWorkflow, runPostCheck, formatCheckReport, saveWorkflowRun } = await import('../workflow.js')
783
+ const wf = loadWorkflow(cwd, 'archive-impact')
784
+ if (wf && changeName) {
785
+ const raw = JSON.stringify(wf)
786
+ const resolved = JSON.parse(raw.replace(/<change-name>/g, changeName))
787
+ const result = runPostCheck(resolved, cwd, progress.project || basename(cwd), {}, specBase)
788
+ // 只报告 impact-analyzer 的结果(doc-syncer 是后续步骤)
789
+ const impactResult = (result.roles || []).find(r => r.id === 'impact-analyzer')
790
+ if (impactResult) {
791
+ const icon = impactResult.status === 'pass' ? '✅' : '❌'
792
+ console.log(`${icon} module-impact.md 检查${impactResult.status === 'pass' ? '通过' : '失败'}`)
793
+ for (const f of (result.failures || []).filter(f => f.role_id === 'impact-analyzer')) {
794
+ // f 是 {level,role_id,output,check,message} 对象——直接 ${f} 打印裸 [object Object](ql-20260819-006);
795
+ // message 才是人类可读 detail(如「缺少章节: …」),缺字段时 stringify 兜底防再退化成不可诊断输出
796
+ console.log(` └─ ${f.message ?? JSON.stringify(f)}`)
797
+ }
798
+ }
799
+ const saved = saveWorkflowRun(result, {
800
+ cwd,
801
+ source: 'run.js',
802
+ stage: 'archive',
803
+ step: steps[currentIdx]?.name,
804
+ ...(platformOpts.runtimeRoot ? { runtimeRoot: platformOpts.runtimeRoot } : {}),
805
+ ...(platformOpts.scanRunId ? { scanRunId: platformOpts.scanRunId } : {})
806
+ })
807
+ if (saved) console.log(`📁 结果已归档:${saved}`)
808
+ }
809
+ } catch (e) {
810
+ console.warn(`⚠️ workflow 检查跳过:${e.message}`)
811
+ }
812
+ }
813
+ return null
814
+ }
815
+ /**
816
+ * quick 阶段完成收尾(W6 Step6b 从 completeStep 内联块抽出):
817
+ * 强校验 QUICKLOG 条目 + 审计(auditQuickCompletion)+ 结果摘要结构校验 + 翻状态/勾 tasks.md
818
+ * (CLI 接管 QUICKLOG 分配/写入/收尾)。blocked → process.exit(1)(无 early-return,调用点纯 await)。
819
+ *
820
+ * ctx:stageName/steps/currentIdx/cwd/progress/changeName/specBase/outputText/confirm/
821
+ * isForceBaseline/isAllowNew/platformOpts。辅助函数直接 import(safeGit/auditQuickCompletion ← shared,
822
+ * printQuickAuditReview ← quick-audit,4 个 quicklog fns ← quicklog,unlinkSync/rmSync ← fs 静态)。
823
+ */
824
+ export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, platformOpts, pm }) {
825
+ // quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
826
+ if (stageName === 'quick') {
827
+ // §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
828
+ // D-003@v1:progress._write 不持久化顶层 quickGuard,跨进程 --done 时读出的 progress 无 quickGuard,
829
+ // 若仍用 if (progress.quickGuard) 驱动收尾会整体跳过,导致 .runtime/quick-sessions/<sessionId>/ 残留僵尸。
830
+ // 改为从文件读 guard:优先 session 目录 guard.json,回退旧单文件 quick-guard.json(task-03 前兼容)。
831
+ // sessionId == changeName == quick-<uuid8>(completeStep 作用域内 changeName 已解构自 options)。
832
+ // session 目录经 resolveQuickSessionsDir 单一解析(multi-agent-review Q4):与 stage.js 写入路径对齐,
833
+ // 平台模式 runtimeRoot 与 specBase/.runtime 不同时不再读不到 guard。
834
+ const sessionGuardFile = join(resolveQuickSessionsDir(platformOpts, specBase), changeName, 'guard.json')
835
+ const legacyGuardFile = join(specBase, '.runtime', 'quick-guard.json')
836
+ let guard = null
837
+ try {
838
+ guard = existsSync(sessionGuardFile)
839
+ ? JSON.parse(readFileSync(sessionGuardFile, 'utf8'))
840
+ : (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
841
+ } catch {}
842
+
843
+ // 强校验 / 收尾:本会话必须有一条真实 QUICKLOG 条目(治「报 SAFE 但漏写」bug)。
844
+ // guard 缺失(brownfield:新代码前启动的会话)不阻断——兜底补写一条记录,保住「完成必有记录」不变量。
845
+ const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
846
+ let qlId = guard?.quicklogId || null
847
+ const linkedChanges = Array.isArray(guard?.linkedChanges) ? guard.linkedChanges : []
848
+ // 审计结果(仅 guard 存在时填充)。提到 if(guard) 外声明,供下方回填 QUICKLOG 文件行复用
849
+ // review.changedFiles;brownfield 无 guard 时保持 null → 文件行不回填(降级,不报错)。
850
+ let review = null
851
+
852
+ // 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
853
+ // task-02:mergedGuard 提升到 if 外声明,供下方并发预检钩子复用与 auditQuickCompletion
854
+ // 同源的 guard 字段(baselineFiles/linkedChanges)。brownfield 无 guard 时保持 null,
855
+ // 钩子 ownFiles/linkedChanges 走 ?? [] 兜底(D-003 不抛 TypeError)。
856
+ let mergedGuard = null
857
+ if (guard) {
858
+ // --done 的 --force-baseline/--allow-new 并入 guard(与 step1 持久化值取或)。
859
+ // 修复 ql-20260713-002-7628:旧代码解析了这两个 flag 但只传 {isConfirm} 给审计,
860
+ // 致 --done --force-baseline 静默无效、用户被误导「重跑 --confirm」也无法解锁。
861
+ mergedGuard = {
862
+ ...guard,
863
+ forceBaseline: guard.forceBaseline || isForceBaseline,
864
+ allowNew: guard.allowNew || isAllowNew,
865
+ allowDelete: guard.allowDelete || isAllowDelete,
866
+ // O-1(docs-signals-o12):specBase/projectName 透传给审计——docSyncHint 模块归属用
867
+ // (平台模式 resolveSpecDir(cwd) 会 miss specRoot 静默丢信号,plan 审查 gap-5)
868
+ specBase,
869
+ projectName: progress?.project || null,
870
+ // 他者 active 会话声明索引(坑 foreign-session-declared-false-block):多 agent 并发时
871
+ // 并行会话 --files 声明的文件退栈归该会话审计,不再误拦本会话。与上方 guard 读取同源
872
+ // (resolveQuickSessionsDir 同参,平台模式 runtimeRoot 对齐);fail-open:采集失败 → 空
873
+ // → 回到无豁免现状。
874
+ otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, specBase, changeName),
875
+ }
876
+ review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
877
+ printQuickAuditReview(review)
878
+ if (review.status === 'blocked') {
879
+ steps[currentIdx].status = 'pending'
880
+ steps[currentIdx].completedAt = null
881
+ if (outputText) steps[currentIdx].output = null
882
+ process.exit(1)
883
+ }
884
+ progress.lastQuickReview = review
885
+ }
886
+
887
+ // task-02 并发预检(FR-05/FR-07,纯副作用 advisory):auditQuickCompletion 返回后、推进前
888
+ // 扫工作树,识别他者未提交改动 / 他者脏变更目录,有则 console.warn。不改 status/gate、
889
+ // 不 exit、不 return early(FR-07 铁律)。ownFiles 必含 baselineFiles(D-001:脏工作树 quick
890
+ // 完成时本会话 baseline 不被他者误报);review/mergedGuard 均可能为 null(brownfield 无 guard,
891
+ // D-003),用 ?. + ?? [] 兜底防 spread undefined 抛 TypeError(B-005)。整钩子 try/catch 隔离
892
+ // ——detect 本就 fail-open,保守再包一层,任何异常只吞不 bubble,主完成流程不受影响。
893
+ try {
894
+ // ownFiles 锚点(2026-08-18 误归属修复):声明会话 = baselineFiles ∪ allowedFiles(声明即归属),
895
+ // 不再用 changedFiles 全量——他者窗口文件已被污染进 changedFiles,旧口径自吞致预检对
896
+ // ql-20260818-003 形态结构性失明。未声明会话维持旧口径(changed ∪ baseline):无声明无归属
897
+ // 信息,收窄锚点会把自身未声明改动全误报他者。resolveConcurrentAnchor 纯函数(concurrent-detect)。
898
+ const ownFiles = resolveConcurrentAnchor({
899
+ changedFiles: review?.changedFiles ?? [],
900
+ baselineFiles: mergedGuard?.baselineFiles ?? [],
901
+ allowedFiles: mergedGuard?.allowedFiles ?? [],
902
+ })
903
+ const detected = detectConcurrentChanges(cwd, {
904
+ changeName,
905
+ linkedChanges: mergedGuard?.linkedChanges ?? [],
906
+ ownFiles,
907
+ })
908
+ const warn = formatConcurrentWarning(detected)
909
+ if (warn) console.warn(warn)
910
+ } catch (e) {
911
+ // fail-open:并发预检异常绝不阻断主完成流程(FR-07)。
912
+ }
913
+
914
+ // 结果摘要结构校验(最后一步、isDone 且带了 --output 时):--output 是 QUICKLOG「结果:」
915
+ // 归档的唯一来源,要求按 需求/根因/方案/结果 模板给全(见 stages/quick.js step3 prompt)。
916
+ // 确定性校验:只查必填字段是否齐全,不判内容质量。缺字段 → 本次不完成(回滚 step 状态 +
917
+ // exit 1),保留「进行中」条目,agent 补全 --output 后重跑 --done 即可,不丢进度。
918
+ // 仅 completeQuicklogEntry 会实际持久化时才校验;前两个 step 的 --done output 不入 QUICKLOG,不校验。
919
+ // Q6:quick 末步 --done 必须带 --output(四字段结果是 QUICKLOG「结果:」唯一来源)。缺则回退 pending +
920
+ // exit(1),不静默落空结果条目(原 if(outputText) 守卫致 outputText 为空时跳过校验,completeQuicklogEntry
921
+ // 又用 outputText||'' 兜底 → 结果块为空却翻「已完成」)。handleQuickStageCompletion 仅在阶段完成(=末步)时触发。
922
+ const isLastStep = currentIdx === steps.length - 1
923
+ if (isLastStep && !outputText) {
924
+ console.error('\n❌ quick 最后一步 --done 必须带 --output(四字段结果模板)。')
925
+ console.error(' --output 是 QUICKLOG「结果:」归档的唯一来源。补全后重跑 --done(不丢进度),推荐四参数形式(CLI 自动合成,无嵌套冒号事故面):')
926
+ console.error(' sillyspec run quick --done --change <changeName> --req "一句话语义化短标题" --cause "为什么这样改" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"')
927
+ console.error(' 兼容旧形式:--output "需求:… 根因:… 方案:… 结果:…"')
928
+ steps[currentIdx].status = 'pending'
929
+ steps[currentIdx].completedAt = null
930
+ process.exit(1)
931
+ }
932
+ if (outputText) {
933
+ const resultCheck = validateQuickResult(outputText)
934
+ if (!resultCheck.ok) {
935
+ console.error('\n' + getRule('quick.result-labels').failMessage.replaceAll('${missing}', resultCheck.missing.join('、')))
936
+ console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
937
+ console.error(` 补全后重跑 --done(不丢进度)。推荐四参数形式(CLI 自动合成结构化模板,避免旧形式嵌套全角冒号被拆分判定缺字段):`)
938
+ console.error(` sillyspec run quick --done --change <changeName> --req "一句话语义化短标题(即 QUICKLOG 条目标题)" --cause "为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"`)
939
+ console.error(` 或照抄旧形式模板:`)
940
+ console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
941
+ console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
942
+ console.error(` 方案:怎么改的`)
943
+ console.error(` 结果:验证情况(测试数 / lint / typecheck / 部署状态)"`)
944
+ steps[currentIdx].status = 'pending'
945
+ steps[currentIdx].completedAt = null
946
+ if (outputText) steps[currentIdx].output = null
947
+ process.exit(1)
948
+ }
949
+ }
950
+
951
+ if (!qlId) {
952
+ // 无 ql-ID(guard 缺失或 brownfield 无 quicklogId):补分配后立即完成,不阻断。
953
+ try {
954
+ const alloc = await allocateQuicklogEntry(specBase, gitUser, {
955
+ description: guard?.taskDescription || '(补分配)',
956
+ linkedChanges,
957
+ allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
958
+ })
959
+ qlId = alloc.qlId
960
+ console.log(`📝 QUICKLOG 兜底补写: ${qlId}(guard 缺失/brownfield 会话)`)
961
+ } catch (e) {
962
+ console.error(`\n❌ QUICKLOG 补分配失败: ${e.message}`)
963
+ steps[currentIdx].status = 'pending'
964
+ steps[currentIdx].completedAt = null
965
+ if (outputText) steps[currentIdx].output = null
966
+ process.exit(1)
967
+ }
968
+ }
969
+ if (!findQuicklogEntry(specBase, gitUser, qlId)) {
970
+ console.error(`\n❌ quick 阶段完成校验失败:QUICKLOG 条目 ${qlId} 不存在。`)
971
+ console.error(` 会话期间记录被删除或从未写入。请检查 .sillyspec/quicklog/ 后重跑 --done。`)
972
+ steps[currentIdx].status = 'pending'
973
+ steps[currentIdx].completedAt = null
974
+ if (outputText) steps[currentIdx].output = null
975
+ process.exit(1)
976
+ }
977
+ // 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
978
+ // resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
979
+ try {
980
+ // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
981
+ // 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
982
+ // 归属口径(2026-08-18 误归属修复):声明会话优先 attributedFiles(窗口∩声明∪同文件并发命中),
983
+ // 他者窗口文件不进文件行;未声明会话/旧 review 无 attributedFiles → 兜底 changedFiles 全量。
984
+ const auditFiles = Array.isArray(review?.attributedFiles)
985
+ ? review.attributedFiles
986
+ : (Array.isArray(review?.changedFiles) ? review.changedFiles : [])
987
+ const realFiles = auditFiles.filter(f => !isQuickMetadata(f, linkedChanges))
988
+ // D-8 落盘(2026-08-18 修):advisory 欠账信号从「纯打印」升级为「随条目落盘」——修复
989
+ // 「欠账已记录(QUICKLOG reasons)」的不实承诺(交叉审查实证 reasons 纯 stdout,事后不可审计)。
990
+ // 两周实测(2026-08-31 裁决,doc-consistency-debt §七)需要分母:信号触发次数必须可追溯。
991
+ const auditNotes = []
992
+ if (review?.docSyncHint && review.docSyncHint.touchedSource > 0 && review.docSyncHint.docFiles.length === 0) {
993
+ const mods = Array.isArray(review.docSyncHint.modules) && review.docSyncHint.modules.length > 0
994
+ ? `(涉及模块:${review.docSyncHint.modules.map((m) => m.id).join(' · ')})` : ''
995
+ auditNotes.push(`📝 文档欠账(D-8):${review.docSyncHint.touchedSource} 个源码文件改动未同步任何模块文档${mods}`)
996
+ }
997
+ if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
998
+ auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
999
+ }
1000
+ // 归属切分注(2026-08-18 误归属修复):窗口内未声明脏文件不进「文件:」行,但必须落盘可追溯
1001
+ // (多 agent 并发仓他者窗口改动 / 本会话漏声明均可能),防真实改动被静默挤走。
1002
+ if (Array.isArray(review?.undeclaredFiles) && review.undeclaredFiles.length > 0) {
1003
+ const undeclared = review.undeclaredFiles.filter(f => !isQuickMetadata(f, linkedChanges))
1004
+ if (undeclared.length > 0) {
1005
+ auditNotes.push(`⚖️ 归属切分:${undeclared.length} 个窗口内未声明脏文件未计入文件行(并行会话改动或本会话漏声明):${undeclared.join(', ')}`)
1006
+ }
1007
+ }
1008
+ await completeQuicklogEntry(specBase, gitUser, qlId, {
1009
+ resultText: outputText || '',
1010
+ linkedChanges,
1011
+ changedFiles: realFiles,
1012
+ auditNotes,
1013
+ })
1014
+ console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
1015
+ // 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
1016
+ // 与 QUICKLOG 标题刷新(flipEntryInContent 内 extractTitleFromResult)同源,保持 DB↔QUICKLOG 一致。
1017
+ try {
1018
+ const refinedTitle = extractTitleFromResult(outputText || '');
1019
+ if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle });
1020
+ } catch { /* title 刷新失败不阻断完成 */ }
1021
+ } catch (e) {
1022
+ console.warn(`⚠️ QUICKLOG 完成态写入失败: ${e.message}`)
1023
+ }
1024
+
1025
+ // 轻量归档任务已全勾选的关联真实变更
1026
+ try {
1027
+ const closeResult = await closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges, platformOpts })
1028
+ if (closeResult.closed.length > 0) {
1029
+ console.log(`📦 已自动归档 ${closeResult.closed.length} 个关联变更:${closeResult.closed.join(', ')}`)
1030
+ }
1031
+ if (closeResult.skipped.length > 0) {
1032
+ for (const s of closeResult.skipped) {
1033
+ console.log(`⏭️ 关联变更 ${s.name} 未归档:${s.reason}`)
1034
+ }
1035
+ }
1036
+ } catch (e) {
1037
+ console.warn(`⚠️ 关联变更轻量归档失败(不阻断 quick 完成): ${e.message}`)
1038
+ }
1039
+
1040
+ // 清理 session 目录(rmSync/unlinkSync 容忍不存在)。路径与写入对齐(Q4 resolveQuickSessionsDir)。
1041
+ try {
1042
+ if (changeName) {
1043
+ const sessionDir = join(resolveQuickSessionsDir(platformOpts, specBase), changeName)
1044
+ rmSync(sessionDir, { recursive: true, force: true })
1045
+ }
1046
+ if (existsSync(legacyGuardFile)) unlinkSync(legacyGuardFile)
1047
+ } catch {}
1048
+
1049
+ // 注销 quick 会话注册的 changes 行(quick-<uuid8>),避免 active 行随每次 quick 单调累积污染
1050
+ // listChanges / doctor / resolveQuickLinkedChanges(multi-agent-review Q1)。quick 是收尾型会话,
1051
+ // 不走 archive,旧代码从不调 unregisterChange → DB 里 active 的 quick-<hex> 行只增不减。
1052
+ // 仅对 quick-<8hex> sessionId 注销——非 sessionId 形态的变更名是旧兼容路径/真实关联变更,
1053
+ // 误注销会把用户真实变更标 archived(与 command.js:569 sessionId 守卫同形正则)。
1054
+ if (changeName && /^quick-[0-9a-f]{8}$/.test(changeName)) {
1055
+ try {
1056
+ pm.unregisterChange(cwd, changeName)
1057
+ } catch (e) {
1058
+ console.warn(`⚠️ 注销 quick 会话 changes 行失败(不阻断完成): ${e.message}`)
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ return null
1064
+ }
1065
+
1066
+ /**
1067
+ * 判定变更 tasks.md 是否全部勾选完成。
1068
+ * 无 tasks.md 或有未勾选项均返回 false(保守策略)。
1069
+ *
1070
+ * @param {string} specBase - .sillyspec 根目录
1071
+ * @param {string} changeName - 变更名
1072
+ * @returns {boolean}
1073
+ */
1074
+ function isChangeTasksComplete(specBase, changeName) {
1075
+ const tasksPath = join(specBase, 'changes', changeName, 'tasks.md')
1076
+ if (!existsSync(tasksPath)) return false
1077
+ try {
1078
+ const content = readFileSync(tasksPath, 'utf8').replace(/\r\n/g, '\n')
1079
+ return !/^-\s*\[\s*\]\s+/m.test(content)
1080
+ } catch {
1081
+ return false
1082
+ }
1083
+ }
1084
+
1085
+ /**
1086
+ * 对单个已完成的关联真实变更执行轻量归档(不校验 plan.md/module-impact.md)。
1087
+ *
1088
+ * @param {Object} opts
1089
+ * @param {ProgressManager} opts.pm
1090
+ * @param {string} opts.cwd
1091
+ * @param {string} opts.specBase
1092
+ * @param {string} opts.changeName
1093
+ * @param {Object} [opts.platformOpts]
1094
+ * @returns {Promise<{closed:boolean, destDir?:string, reason?:string}>}
1095
+ */
1096
+ async function closeSingleQuickLinkedChange({ pm, cwd, specBase, changeName, platformOpts = {} }) {
1097
+ const srcDir = join(specBase, 'changes', changeName)
1098
+ if (!existsSync(srcDir)) {
1099
+ return { closed: false, reason: '源目录不存在' }
1100
+ }
1101
+ const changesDir = join(specBase, 'changes')
1102
+ const archiveDir = join(changesDir, 'archive')
1103
+ const date = new Date().toISOString().slice(0, 10)
1104
+ const destName = archiveDestDirName(date, changeName)
1105
+ const destDir = join(archiveDir, destName)
1106
+
1107
+ // 幂等:目标目录已存在或已在 archive/ 中
1108
+ if (existsSync(destDir) || findAlreadyArchivedDir(archiveDir, changeName)) {
1109
+ return { closed: false, reason: `目标目录已存在(${destName})` }
1110
+ }
1111
+
1112
+ mkdirSync(archiveDir, { recursive: true })
1113
+ try {
1114
+ renameSyncRetry(srcDir, destDir)
1115
+ } catch (e) {
1116
+ return { closed: false, reason: `移动目录失败:${e.message}` }
1117
+ }
1118
+
1119
+ // 终态一致化同标准归档(坑 manual-archive-desync-status-only):轻量归档路径不能只翻 status
1120
+ pm.unregisterChange(cwd, changeName, { archiveStepNames: typeof pm.archiveStepNamesForArchive === 'function' ? pm.archiveStepNamesForArchive() : null })
1121
+ await archiveWorktreeCleanup(cwd, changeName, specBase, platformOpts)
1122
+ safeGit(cwd, ['add', '--', `.sillyspec/changes/archive/${destName}/`])
1123
+
1124
+ console.log(`📦 关联变更已自动归档:${changeName} → archive/${destName}/`)
1125
+ return { closed: true, destDir }
1126
+ }
1127
+
1128
+ /**
1129
+ * quick 轻量归档的阶段闸允许集:仅「从未进入完整流程」的变更可被 quick --done 自动归档
1130
+ * (d192f89 原始场景:评估 small 转 quick、停在 brainstorm 的僵尸变更)。execute 完成后
1131
+ * tasks.md 必然全勾选而流程未收尾——不看 current_stage 会把 verify/中途的完整流程变更
1132
+ * 绕过 verify/archive 校验直接归档注销(quick-close-midflight 缺陷)。
1133
+ *
1134
+ * 阶段名允许集之外还有「阶段完成态」闸(ql-20260819-010,quick-done-autoarchive-misfire
1135
+ * 缺陷①):current_stage=brainstorm 但该阶段 status=completed(brainstorm 收尾到 plan
1136
+ * 开始之间的空窗)≠ 僵尸变更——此时关联 quick --done 会把即将进 plan 的进行中变更误
1137
+ * 轻量归档。stage_status=completed 一律走原流程收尾。
1138
+ */
1139
+ const QUICK_CLOSE_ALLOWED_STAGES = new Set(['', 'scan', 'brainstorm'])
1140
+
1141
+ /**
1142
+ * quick --done 完成后,自动关闭任务已全部完成的关联真实变更。
1143
+ * quick-<hex> sessionId 自身不在此处理(由调用方单独注销)。
1144
+ * 单个归档失败 catch warn,不阻断 quick 完成。
1145
+ *
1146
+ * 阶段闸(fail-closed):先查 changes.current_stage——无 DB 记录(未注册目录桩)或停在
1147
+ * scan/brainstorm 才继续 tasks 判定;plan/execute/verify/archive 一律 skip 走原流程收尾。
1148
+ * pm 缺 getChangeStage 接口或查询抛错同样 skip,不静默放行。
1149
+ *
1150
+ * @param {Object} opts
1151
+ * @param {ProgressManager} opts.pm
1152
+ * @param {string} opts.cwd
1153
+ * @param {string} opts.specBase
1154
+ * @param {string[]} [opts.linkedChanges]
1155
+ * @param {Object} [opts.platformOpts]
1156
+ * @returns {Promise<{closed:string[], skipped:{name:string,reason:string}[]}>}
1157
+ */
1158
+ export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges = [], platformOpts = {} }) {
1159
+ const closed = []
1160
+ const skipped = []
1161
+ // 只处理真实变更,跳过 quick 会话 sessionId
1162
+ const realChanges = linkedChanges.filter((name) => !/^quick-[0-9a-f]{8}$/.test(name))
1163
+ for (const changeName of realChanges) {
1164
+ try {
1165
+ if (typeof pm.getChangeStage !== 'function') {
1166
+ skipped.push({ name: changeName, reason: '进度库接口缺失(getChangeStage),无法判定流程阶段,不自动归档' })
1167
+ continue
1168
+ }
1169
+ const stageInfo = pm.getChangeStage(cwd, changeName)
1170
+ if (stageInfo !== null && !QUICK_CLOSE_ALLOWED_STAGES.has(stageInfo.current_stage || '')) {
1171
+ skipped.push({
1172
+ name: changeName,
1173
+ reason: `变更处于完整流程「${stageInfo.current_stage}」阶段(tasks.md 全勾不等于流程收尾),不自动归档——请走原流程收尾(sillyspec progress show 查看进度)`,
1174
+ })
1175
+ continue
1176
+ }
1177
+ // ql-20260819-010(quick-done-autoarchive-misfire 缺陷①):当前阶段已完成 ≠
1178
+ // 从未进入完整流程的僵尸。brainstorm 收尾到 plan 开始之间的空窗里 current_stage
1179
+ // 仍读 brainstorm,只看阶段名会把即将进 plan 的变更误轻量归档(propose 骨架
1180
+ // tasks.md 除 quick 追加的 ql 行外没有任务行,「无未勾选框=全勾」恒真)。
1181
+ // stage_status=completed 一律原流程收尾;null(无阶段行/brownfield)与旧调用方
1182
+ // mock 缺字段(undefined)均按未完成放行,僵尸逃生通道行为不变。
1183
+ if (stageInfo !== null && stageInfo.stage_status === 'completed') {
1184
+ skipped.push({
1185
+ name: changeName,
1186
+ reason: `变更当前阶段「${stageInfo.current_stage || '(空)'}」已完成(推进/收尾中,非僵尸变更),不自动归档——请走原流程收尾(sillyspec progress show 查看进度)`,
1187
+ })
1188
+ continue
1189
+ }
1190
+ if (!isChangeTasksComplete(specBase, changeName)) {
1191
+ skipped.push({ name: changeName, reason: 'tasks.md 未全勾选或不存在' })
1192
+ continue
1193
+ }
1194
+ const result = await closeSingleQuickLinkedChange({ pm, cwd, specBase, changeName, platformOpts })
1195
+ if (result.closed) {
1196
+ closed.push(changeName)
1197
+ } else {
1198
+ skipped.push({ name: changeName, reason: result.reason || '未知原因' })
1199
+ }
1200
+ } catch (e) {
1201
+ console.warn(`⚠️ 关联变更 ${changeName} 轻量归档失败(不阻断 quick 完成): ${e.message}`)
1202
+ skipped.push({ name: changeName, reason: e.message })
1203
+ }
1204
+ }
1205
+ return { closed, skipped }
1206
+ }
1207
+
1208
+ /**
1209
+ * execute「Wave N 执行」步骤完成后扫 worktree 提取 provider endpoint artifact(W6 Step6c 从
1210
+ * completeStep 内联块抽出)。供 verify 阶段 parity 对账 + consumer task 上游契约注入。
1211
+ * 接线自 contract-matrix pipeline。step 级(每个 Wave 执行步骤后跑),无 early-return(try/catch warn)。
1212
+ *
1213
+ * ctx:stageName/steps/currentIdx/changeName/specBase/cwd。extractArtifactsForChange ← 动态 ../contract-matrix.js,
1214
+ * WorktreeManager ← 动态 ../worktree.js(真环依赖保留动态)。
1215
+ */
1216
+ export async function handleExecuteWaveArtifact({ stageName, steps, currentIdx, changeName, specBase, cwd }) {
1217
+ if (stageName === 'execute' && /^Wave \d+ 执行$/.test(steps[currentIdx]?.name || '')) {
1218
+ try {
1219
+ const { extractArtifactsForChange } = await import('../contract-matrix.js')
1220
+ let worktreePath = null
1221
+ try {
1222
+ const { WorktreeManager } = await import('../worktree.js')
1223
+ const meta = new WorktreeManager({ cwd }).getMeta(changeName)
1224
+ if (meta?.worktreePath && existsSync(meta.worktreePath)) worktreePath = meta.worktreePath
1225
+ } catch {}
1226
+ const msg = extractArtifactsForChange({ changeDir: join(specBase, 'changes', changeName), specBase, changeName, worktreePath })
1227
+ if (msg) console.log(msg)
1228
+ } catch (e) { console.warn(`⚠️ 契约 artifact 提取跳过: ${e?.message || e}`) }
1229
+ }
1230
+ return null
1231
+ }
1232
+
1233
+ /**
1234
+ * 聚合最新 execute run 各 task review.json 的 changedFiles(主仓 repo 过滤)。
1235
+ *
1236
+ * 复用 resolveLatestExecuteRunIdWithTasks(task-review.js:684,规避 marker 漂移)+ readReview。
1237
+ * 仅主仓 repo(review.repo 缺省或 'main')的 changedFiles 参与主仓核验;跨仓 repo 的 task 文件
1238
+ * 由跨仓仓独立落地,不在主仓 worktree/分支,混入会误报 missing(Grill M11)。
1239
+ *
1240
+ * @param {{ runtimeRoot: string, changeName: string }} opts
1241
+ * @returns {Promise<string[]>} 主仓 changedFiles 聚合(去重保序);无 run / 无 tasks / 读取失败 → []
1242
+ */
1243
+ export async function collectExecuteChangedFiles({ runtimeRoot, changeName }) {
1244
+ if (!runtimeRoot || !changeName) return []
1245
+ const { resolveLatestExecuteRunIdWithTasks, readReview, normalizeRepoKey } = await import('../task-review.js')
1246
+ // changeName 透传:run 解析按 change 戳优先归属(坑 worktree-cleanup-marker-chain),
1247
+ // 避免 marker 断裂后聚合到其他变更 run 的 changedFiles 误报 missing
1248
+ const runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName })
1249
+ if (!runId) return []
1250
+ const tasksDir = join(runtimeRoot, 'execute-runs', runId, 'tasks')
1251
+ if (!existsSync(tasksDir)) return []
1252
+ let taskIds
1253
+ try {
1254
+ taskIds = readdirSync(tasksDir)
1255
+ } catch {
1256
+ return []
1257
+ }
1258
+ const files = []
1259
+ for (const taskId of taskIds) {
1260
+ const r = readReview(join(tasksDir, taskId, 'review.json'))
1261
+ if (!r.ok || !r.review) continue
1262
+ // 跨仓 repo 过滤:仅主仓(repo 缺省视 'main')参与主仓核验,避免误报
1263
+ if (normalizeRepoKey(r.review.repo) !== 'main') continue
1264
+ if (Array.isArray(r.review.changedFiles)) {
1265
+ for (const f of r.review.changedFiles) {
1266
+ // 交付物过滤(与 worktree.js hasUnappliedChanges isDeliverable 同口径):.sillyspec/ 蓝图/
1267
+ // runtime 产物与 meta.json 不参与落盘核验——它们随主仓/并行 session 维护,不在 worktree 分支,
1268
+ // 混入会误报 missing
1269
+ if (typeof f !== 'string' || f.trim() === '') continue
1270
+ if (f.startsWith('.sillyspec/') || f === 'meta.json') continue
1271
+ files.push(f)
1272
+ }
1273
+ }
1274
+ }
1275
+ return [...new Set(files)]
1276
+ }
1277
+
1278
+ /**
1279
+ * execute 阶段级核验(防空跑谎报,D-002@v1 / FR-04/05/06):聚合最新 execute run 各 task review 声称的
1280
+ * 交付文件,用 findMissingDeliverables 核验其存在于 worktree 分支 tree 或 worktree 工作区。
1281
+ *
1282
+ * - missing 文件 → console.warn 列清单,提示"apply 将无源可复制"(宽松非阻断,不 exit 不 throw)。
1283
+ * - checked:false(worktree 目录 / 分支不存在)→ 保守提示"无法核验,请人工确认"。
1284
+ * - 与 Task Review Gate 既有校验(零改动伪造 / 不相交伪造,task-review.js:590-623)互补不重复拦截:
1285
+ * 本核验的真实增量窗口是「review 通过后文件被删且未 commit」+「无法核验时给人工确认提示」。
1286
+ *
1287
+ * 由 handleExecuteWorktreeCleanup 开头调用:核验发生在 worktree cleanup 之前,目录被清前先判定
1288
+ * 交付文件是否落盘。整个钩子 try/catch 兜底——任何异常只 warn,绝不影响 execute 完成(FR-07)。
1289
+ *
1290
+ * ctx:stageName/changeName/cwd。WorktreeManager + findMissingDeliverables ← 动态 ../worktree.js;
1291
+ * task-review 读取 ← 动态 ../task-review.js。runtimeRoot 用本地 specBase 解析(无 platformOpts 传入
1292
+ * 时平台 runtimeRoot 不在本函数作用域,降级为本仓 .runtime,读不到 review 则跳过,零误报)。
1293
+ */
1294
+ export async function handleExecuteDeliverableCheck({ stageName, changeName, cwd }) {
1295
+ if (stageName !== 'execute' || !changeName) return null
1296
+ try {
1297
+ const { WorktreeManager, findMissingDeliverables } = await import('../worktree.js')
1298
+ const wm = new WorktreeManager({ cwd })
1299
+ const meta = wm.getMeta(changeName)
1300
+ if (!meta) return null // 无 worktree meta(非 worktree execute / 已清),无需核验
1301
+ const specBase = join(cwd, '.sillyspec')
1302
+ const runtimeRoot = resolveRuntimeRoot({}, specBase)
1303
+ const changedFiles = await collectExecuteChangedFiles({ runtimeRoot, changeName })
1304
+ if (changedFiles.length === 0) return null
1305
+ const { missing, checked } = findMissingDeliverables({
1306
+ worktreePath: meta.worktreePath,
1307
+ branch: meta.branch,
1308
+ changedFiles,
1309
+ })
1310
+ if (!checked) {
1311
+ console.warn('⚠️ execute 阶段级核验:无法核验(worktree 目录或分支不存在),请人工确认交付文件已落盘。')
1312
+ return null
1313
+ }
1314
+ if (missing.length > 0) {
1315
+ console.warn(`⚠️ execute 阶段级核验:以下 ${missing.length} 个声称实现的交付文件既不在分支也不在工作区,疑似空跑/从未落盘:`)
1316
+ for (const f of missing) console.warn(` ${f}`)
1317
+ console.warn(' 请检查子代理是否真实实现,或先 commit 到分支;apply 将无源可复制。')
1318
+ }
1319
+ } catch (e) {
1320
+ console.warn(`⚠️ execute 阶段级核验跳过: ${e?.message || e}`)
1321
+ }
1322
+ return null
1323
+ }
1324
+
1325
+ /**
1326
+ * execute 阶段完成时条件性清理 worktree(W6 Step6c 从 completeStep 完成路径内联块抽出)。
1327
+ * 不依赖 AI agent 的完成确认步骤:有未 apply 变更 → 保留 worktree;否则 cleanup(含 in-place 安全清理)。
1328
+ * stage 级(execute 阶段全部完成时跑),无 early-return(try/catch warn)。
1329
+ *
1330
+ * ctx:stageName/changeName/cwd。WorktreeManager ← 动态 ../worktree.js。
1331
+ */
1332
+ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd }) {
1333
+ // 阶段级核验(D-002@v1,防空跑谎报):cleanup 之前判定 review 声称实现的交付文件是否落盘。
1334
+ // 宽松非阻断:缺失 warn / 无法核验保守提示 / 异常只 warn,均不影响下方 cleanup 与 execute 完成。
1335
+ await handleExecuteDeliverableCheck({ stageName, changeName, cwd })
1336
+ if (stageName === 'execute' && changeName) {
1337
+ try {
1338
+ const { WorktreeManager } = await import('../worktree.js');
1339
+ const wm = new WorktreeManager({ cwd });
1340
+ const meta = wm.getMeta(changeName);
1341
+ if (!meta) {
1342
+ console.log('🔗 Worktree: n/a (no meta)');
1343
+ } else if (meta.mode === 'native-worktree') {
1344
+ console.log('🔗 Worktree: kept (外部隔离环境)');
1345
+ } else {
1346
+ // in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)。
1347
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):检查+清理与并行会话的 apply/cleanup 互斥
1348
+ // (防 TOCTOU:检查时他者正在 apply → 判定漂移)。best-effort:锁超时不阻断 execute
1349
+ // 完成,降级为保留 worktree + 手动清理指引。
1350
+ try {
1351
+ const { withMainRepoLock } = await import('../worktree-apply.js')
1352
+ const cleanResult = await withMainRepoLock(cwd, changeName, 'execute-cleanup', () => {
1353
+ const check = wm.hasUnappliedChanges(changeName);
1354
+ if (check.hasChanges) {
1355
+ return { kept: true, check }
1356
+ }
1357
+ return { kept: false, result: wm.cleanup(changeName) }
1358
+ })
1359
+ if (cleanResult.kept) {
1360
+ console.log(`🔗 Worktree: pending apply (${cleanResult.check.changedFiles.length} 个未应用变更)`);
1361
+ console.log(` 下一步: sillyspec worktree apply ${changeName}`);
1362
+ } else {
1363
+ console.log(`🔗 Worktree: ${cleanResult.result.result}`);
1364
+ if (cleanResult.result.residual?.length > 0) {
1365
+ console.warn(` ⚠️ 清理残留: ${cleanResult.result.residual.join('; ')}`);
1366
+ console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
1367
+ } else if (cleanResult.result.details?.length > 0) {
1368
+ for (const d of cleanResult.result.details) {
1369
+ if (d.startsWith('⚠️')) console.log(` ${d}`);
1370
+ }
1371
+ }
1372
+ }
1373
+ } catch (lockErr) {
1374
+ if (/互斥锁被占用/.test(String(lockErr.message))) {
1375
+ console.log(`🔗 Worktree 自动清理跳过(主仓互斥锁被他者会话持有)——稍后手动: sillyspec worktree cleanup ${changeName}`);
1376
+ } else {
1377
+ console.warn(`⚠️ worktree 清理异常(不阻断): ${lockErr.message}`);
1378
+ }
1379
+ }
1380
+ }
1381
+ } catch (e) {
1382
+ console.warn(`🔗 Worktree: check failed — ${e.message}`);
1383
+ }
1384
+ }
1385
+ return null
1386
+ }
1387
+ /**
1388
+ * scan 阶段完成后处理(W6 Step6d 从 completeStep 完成路径内联块抽出):
1389
+ * - 平台模式(specRoot/runtimeRoot):写 manifest.json + 跑 scan-postcheck + 结构化结果 +
1390
+ * 更新平台指针(SCAN_COMPLETED)+ failed_post_check 阻断(exit 1 / early-return)
1391
+ * - 非平台模式:轻量 postcheck + 结构化结果写 .runtime/
1392
+ * 返回 early-return 对象(platform failed_post_check 非 exit 路径)由 completeStep 透传;null = 放行。
1393
+ *
1394
+ * ctx:stageName/currentIdx/cwd/progress/pm/stageData/changeName/outputText/platformOpts。
1395
+ * safeGit/triggerSync ← shared;writeAtomicSync ← fs-atomic;SCAN_STATUS/POINTER_STATUS ← constants;
1396
+ * mkdirSync/writeFileSync/readFileSync/unlinkSync/join ← 顶部静态;runScanPostCheck 等 ← 动态 ../scan-postcheck.js。
1397
+ *
1398
+ * 搬迁清理:删 4 个冗余动态 builtin import(fs/path/child_process,execSync 死代码)+ _readFileSync 别名改回 readFileSync。
1399
+ */
1400
+ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, progress, pm, stageData, changeName, outputText, platformOpts }) {
1401
+ // 平台模式:scan 完成后生成 manifest.json + post-check
1402
+ if (stageName === 'scan' && (platformOpts.specRoot || platformOpts.runtimeRoot)) {
1403
+ if (!platformOpts.specRoot) {
1404
+ // 只传 runtimeRoot 无 specRoot(command.js 允许的组合):manifest 无处落盘。
1405
+ // 显式 fail-closed 报错——此前 mkdirSync(null) 抛 TypeError 被外层 catch 吞掉,
1406
+ // postcheck/指针升级/阻断全部静默跳过,scan 失败也"干净成功"(体检 BUG-12)
1407
+ console.error(`❌ 平台模式缺少 --spec-root,无法写 manifest.json(scan postcheck 中止)`)
1408
+ stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.manifestWritten = false
1409
+ stageData.scanMeta.manifestError = 'missing specRoot'
1410
+ return
1411
+ }
1412
+ try {
1413
+ stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.manifestWritten = false; // 默认失败
1414
+ const manifestDir = platformOpts.specRoot
1415
+ mkdirSync(manifestDir, { recursive: true })
1416
+ let sourceCommit = null
1417
+ let sourceCommitError = null
1418
+ try {
1419
+ const gitResult = safeGit(cwd, ['rev-parse', 'HEAD'])
1420
+ sourceCommit = gitResult.value
1421
+ sourceCommitError = gitResult.error
1422
+ } catch (e) {
1423
+ sourceCommitError = e.message
1424
+ }
1425
+ const manifest = {
1426
+ workspace_id: platformOpts.workspaceId || null,
1427
+ scan_run_id: platformOpts.scanRunId || null,
1428
+ source_root: cwd,
1429
+ spec_root: platformOpts.specRoot || null,
1430
+ runtime_root: platformOpts.runtimeRoot || null,
1431
+ source_commit: sourceCommit,
1432
+ source_commit_error: sourceCommit === null ? (sourceCommitError || 'unknown') : undefined,
1433
+ generated_at: new Date().toISOString(),
1434
+ schema_version: 1,
1435
+ scan_profile: stageData.scanProfile
1436
+ ? { mode: stageData.scanProfile.mode, reason: stageData.scanProfile.reason }
1437
+ : null,
1438
+ postcheck_result_path: null,
1439
+ workflow_runs_dir: platformOpts.runtimeRoot
1440
+ ? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown', 'workflow-runs')
1441
+ : null,
1442
+ platform_pointer_path: join(cwd, '.sillyspec-platform.json'),
1443
+ platform_pointer_status: POINTER_STATUS.ACTIVE,
1444
+ }
1445
+ const manifestPath = join(manifestDir, 'manifest.json')
1446
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
1447
+ console.log(`📄 manifest.json 已写入: ${manifestPath}`)
1448
+ stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.manifestWritten = true;
1449
+ if (!sourceCommit) {
1450
+ console.log(`⚠️ source_commit 无法获取(可能非 git 目录),已设为 null`)
1451
+ }
1452
+ // 清理平台参数临时文件
1453
+ const platformOptsFile = join(manifestDir, '.runtime', 'platform-scan.json')
1454
+ try { unlinkSync(platformOptsFile) } catch {}
1455
+
1456
+ // CLI 层 post-check(替代旧的简单检查)
1457
+ const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
1458
+ const postResult = runScanPostCheck({
1459
+ cwd,
1460
+ specDir: platformOpts.specRoot,
1461
+ outputText,
1462
+ scanMeta: {
1463
+ projectListParsed: stageData.scanMeta?.projectListParsed ?? null,
1464
+ manifestWritten: stageData.scanMeta?.manifestWritten ?? null,
1465
+ },
1466
+ scanProfile: stageData.scanProfile || null,
1467
+ })
1468
+ printScanPostCheckResult(postResult)
1469
+
1470
+ // 生成结构化 JSON 并写入 runtime(供 SillyHub 消费)
1471
+ const structured = formatStructuredResult(postResult, {
1472
+ workspace_id: platformOpts.workspaceId,
1473
+ scan_run_id: platformOpts.scanRunId,
1474
+ source_root: cwd,
1475
+ spec_root: platformOpts.specRoot,
1476
+ runtime_root: platformOpts.runtimeRoot,
1477
+ })
1478
+ const postcheckJsonPath = writeStructuredResult(structured, platformOpts.specRoot, {
1479
+ runtimeRoot: platformOpts.runtimeRoot,
1480
+ scanRunId: platformOpts.scanRunId,
1481
+ })
1482
+ if (postcheckJsonPath) {
1483
+ console.log(`📄 postcheck-result.json 已写入: ${postcheckJsonPath}`)
1484
+ manifest.postcheck_result_path = postcheckJsonPath
1485
+ }
1486
+
1487
+ // 将 post-check 结果写入 manifest
1488
+ manifest.scan_post_check = {
1489
+ status: postResult.status,
1490
+ checks: postResult.checks,
1491
+ }
1492
+ // 更新 manifest
1493
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
1494
+ console.log(`📄 manifest.json 已更新(含 post-check 结果)`)
1495
+
1496
+ // 更新平台指针状态为 scan_completed
1497
+ const pointerPath = join(cwd, '.sillyspec-platform.json')
1498
+ try {
1499
+ const pointer = JSON.parse(readFileSync(pointerPath, 'utf8'))
1500
+ pointer.status = POINTER_STATUS.SCAN_COMPLETED
1501
+ pointer.completedAt = new Date().toISOString()
1502
+ pointer.scanStatus = postResult.status
1503
+ writeAtomicSync(pointerPath, JSON.stringify(pointer, null, 2) + '\n')
1504
+ } catch (e) {
1505
+ // 不阻断 scan 主流程,但暴露失败——pointer 写失败会让平台看不到 scan_completed,
1506
+ // 与项目 fail-loud 原则一致:宁可可见地 warn,也不静默吞错。
1507
+ console.warn(`⚠️ 更新平台指针状态失败(scan_completed 可能未落盘): ${e.message}`)
1508
+ }
1509
+
1510
+ // failed_post_check 时强制阻止 clean success
1511
+ if (postResult.status === 'failed_post_check') {
1512
+ stageData.status = SCAN_STATUS.FAILED_POST_CHECK
1513
+ stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
1514
+ pm._write(cwd, progress, changeName)
1515
+ triggerSync(cwd, changeName, platformOpts)
1516
+ console.error(`\n❌ scan post-check 失败,状态设为 failed_post_check。不允许 clean success。`)
1517
+ console.error(` 请检查上方错误信息并修复后重新 scan。`)
1518
+ // 平台模式:exit(1) 让 daemon/SillyHub 感知非 0 退出码(manifest.json 已落盘,不会被撤销)
1519
+ if (platformOpts.specRoot || platformOpts.runtimeRoot) {
1520
+ console.error(' 平台模式:CLI 将以 exit code 1 退出,通知 SillyHub scan 失败。')
1521
+ process.exit(1)
1522
+ }
1523
+ // 接口与 plan contract (run.js:2551 附近 plan 失败分支) 对齐:
1524
+ // 返回 { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
1525
+ // 让上层 runStage 走"完成但不推进"分支,--done 被拒
1526
+ return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
1527
+ } else if (postResult.status === 'completed_with_warnings') {
1528
+ // 警告不阻止完成,但记录
1529
+ stageData.status = 'completed'
1530
+ stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
1531
+ pm._write(cwd, progress, changeName)
1532
+ }
1533
+ } catch (e) {
1534
+ console.warn(`⚠️ manifest.json 写入失败: ${e.message}`)
1535
+ }
1536
+ }
1537
+
1538
+ // 非 platform 模式 scan 也做轻量 post-check + 结构化输出
1539
+ if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
1540
+ const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
1541
+ const postResult = runScanPostCheck({ cwd, specDir: null, outputText, scanProfile: stageData.scanProfile || null })
1542
+ printScanPostCheckResult(postResult)
1543
+ // 结构化结果写入 .sillyspec/.runtime/
1544
+ const structured = formatStructuredResult(postResult, { source_root: cwd })
1545
+ const postcheckJsonPath = writeStructuredResult(structured, join(cwd, '.sillyspec'))
1546
+ if (postcheckJsonPath) {
1547
+ console.log(`📄 postcheck-result.json 已写入: ${postcheckJsonPath}`)
1548
+ }
1549
+ }
1550
+ return null
1551
+ }
1552
+