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,2798 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SillySpec CLI — 安装工具
5
+ *
6
+ * 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
7
+ * 状态管理通过 sillyspec.db(SQLite)完成,使用 `sillyspec progress` 命令。
8
+ */
9
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
10
+ import { writeAtomicSync } from './fs-atomic.js';
11
+ import { basename, extname, join, resolve } from 'path';
12
+ import { safeGit, git } from './git-helper.js';
13
+ import { getVersion } from './version.js';
14
+
15
+ // E22(性能#2):progress.js(拖 db.js→node:sqlite 链 ~48ms)与 run/shared.js(拖 stages 全家
16
+ // ~30ms)原先为顶层静态 import——最轻命令(--version/help)白付 ~78ms 启动税。两条链无模块级
17
+ // 使用点(printUsage 纯文本),改在 main() 早退之后动态加载,轻路径零加载;重路径首个使用点
18
+ // 之前完成加载,行为零变化。
19
+
20
+ // ── CLI 入口 ──
21
+
22
+ // did-you-mean 从 ./run/shared.js 复用(命令级 + flag 级 typo 建议)。E22:同上改惰性动态加载。
23
+ function printUsage() {
24
+ console.log(`
25
+ SillySpec CLI — 规范驱动开发工具包
26
+
27
+ 用法:
28
+ sillyspec init [--tool <name>] [--interactive] [--dir <path>] [--spec-dir <path>]
29
+ 初始化(零交互,自动检测工具并安装命令模板)
30
+ sillyspec setup [--list] 安装推荐 MCP 工具(--list 查看已安装状态)
31
+
32
+ sillyspec run <stage> [options] 执行阶段步骤(核心命令)
33
+ sillyspec <stage> [options] 顶层别名,等同 run <stage>
34
+ stage: scan | brainstorm | plan | execute | verify | archive |
35
+ quick | explore | status | doctor | auto
36
+
37
+ run 通用参数(所有 stage 适用):
38
+ --done --output "摘要" [--input "用户原话"] 完成当前步骤
39
+ --status 查看阶段进度
40
+ --skip 跳过可选步骤
41
+ --reset 重置阶段(从头开始)
42
+ --reopen --from-step <序号|名称> 重新打开已完成阶段进入修订
43
+ --wait --reason "..." --options "A,B" 暂停等用户决策
44
+ --continue --answer "..." 恢复等待中的步骤
45
+ --done --answer "..." --output "..." 一步完成 wait+done
46
+ --change <名> 指定变更名(多活跃变更必填,单变更可省)
47
+ --spec-dir <path> 指定规范目录(默认 <项目>/.sillyspec)
48
+ --non-interactive CI/脚本:禁用交互式 prompt
49
+ --interactive 强制交互(即便 stdin 非 TTY)
50
+ --skip-approval 跳过审批/校验门控(需明确意图)
51
+ --json 输出 JSON(程序化读取)
52
+
53
+ 阶段特有参数:
54
+ quick: --linked-changes none|a,b 显式关联变更(取代 --change,推荐)
55
+ --files a.js,b.js 显式声明允许修改的文件
56
+ --allow-new 允许新增文件(默认禁止)
57
+ --force-baseline 允许覆盖 baseline 受保护文件
58
+ --confirm 完成时确认接受变更审计
59
+ scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
60
+ --force-rescan 覆盖已有 scan 文档保护
61
+ --diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
62
+ (子命令: sillyspec scan diff 等价 --diff)
63
+ archive: --confirm 归档确认(必须)
64
+ auto: --mode <模式> 显式指定流程模式
65
+ 平台: --runtime-root <path> / --workspace-id <id> / --scan-run-id <id>
66
+
67
+ sillyspec progress <cmd> 进度记录(轻量,不强制顺序)
68
+ init | show | set-stage <stage> | add-step <stage> <name> |
69
+ update-step <stage> <name> --status <st> [--output <t>] [--force] | complete-stage <stage> [--force] |
70
+ check | repair [--apply] | validate | reset [--stage X] |
71
+ batch --total N --completed M [--failed F] [--skipped K] | batch --status
72
+
73
+ sillyspec worktree <cmd> git worktree 隔离管理(execute 阶段相关)
74
+ create <change> [--base <branch>] 创建隔离 worktree
75
+ apply <change> [--check-only] 校验并应用变更到主工作区
76
+ assess <change> 风险审计 + 自动 apply
77
+ list | meta <change> 列出 / 读取 meta.json
78
+ cleanup <change> [--force] 清理 worktree
79
+ doctor [--fix] [--stale-hours N] 健康检查 + 修复
80
+ --cleanup-ghosts [--confirm] 归档幽灵 active 记录(db 有行无目录 / 空壳目录 0 文件超 7 天,SS-2/2b)
81
+
82
+ sillyspec local detect [--dir <path>] 生成本地配置 local.yaml(纯 fs 嗅探,零 token、不跑 scan)
83
+ sillyspec local register-repo <key> <path> 注册跨仓仓到 local.yaml repos: 段(外科手术式写入,其余内容保留)
84
+ sillyspec config [schema] [--json] 打印 local.yaml 全部已知键 + 生效状态 + 读取点(堵外部 agent 配置发现缺口)
85
+ sillyspec config cat [--json] 读取 local.yaml 实际内容(自动定位真实路径,worktree 内解析到主仓)
86
+ sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
87
+ sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
88
+ sillyspec agent-log [--detect] [--json] 本地 agent 会话日志查询/现场探测(run 命令自动上报平台 POST /api/agent-logs 并本地留底)
89
+
90
+ sillyspec workflow check <name> [--project <p>] [--change <c>] [--json] [--save]
91
+ 全局 workflow 校验(--save 归档到 .sillyspec/.runtime/workflow-runs/)
92
+ sillyspec workflow list
93
+ sillyspec gate <stage> --change <name> [--json] 机器门控:阶段能否标记完成(只读)
94
+ sillyspec derive <facet> --change <name> [--json] 单项事实核验(facet: execute-evidence|verify-test|task-reviews|artifacts)
95
+ sillyspec backfill-reviews --change <name> [--adopt] [--json] 缺 review.json 的 task 生成草稿;--adopt 重算代填已有 review 的 base/head 等机械字段(verdict 保留)
96
+ sillyspec symbol-impact --change <name> 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(gate 拒绝未替换占位,防骨架直接过门)
97
+ sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
98
+ sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
99
+ sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
100
+ sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
101
+ sillyspec endpoints extract --change <name> [--task task-NN] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
102
+ sillyspec scan-fix-headers [--project <名>] scan 文档补 author/created_at header(幂等)
103
+ sillyspec workspace add <名> <路径> [--role] [--repo] | remove <名> | status 多项目工作区登记与状态探测
104
+ sillyspec scan-fix-leak --spec-dir <specRoot> source_root 误写产物搬到平台 specRoot(污染一键修复)
105
+ sillyspec run quick --cancel --change <会话ID> [--ql <ql-id>] 取消误启动的 quick 会话(QUICKLOG 翻已取消 + 挂载行移除)
106
+ sillyspec register-stage-review --change <name> (--stage <brainstorm|plan|execute> | --all) [--from <review.json>] [--refresh-hash] [--json]
107
+ 生成/adopt stage 级 review.json(docHash 自动算 + 写 marker,治 tier=independent marker 死锁)
108
+
109
+ sillyspec doctor [子命令] 进度库健康检查 + 修复(顶层命令,非 worktree doctor)
110
+ (无子命令) 跑诊断,列出问题
111
+ --align-execute-progress [--change <名>] [--confirm] 基于 plan.md 对齐 execute 派生戳(默认 dry-run)
112
+ --cleanup-remnant [--confirm] 删除 0 字节空占位 db(默认 dry-run,仅 --confirm 落盘)
113
+ --dump-db --path <db 路径> dump 指定 db 内容到文件(取证用)
114
+ --json 结构化诊断 + 落盘 .sillyspec/.runtime/doctor-diagnosis.json
115
+ sillyspec modules <rebuild | status | migrate>
116
+ sillyspec taskcard <change> --task task-01[,task-02...] | --all [--force]
117
+ 生成 Windows 安全 TaskCard 骨架(LF 行尾 + frontmatter 闭合 +
118
+ 硬校验 9 字段齐全,标题自动取自 plan.md,Edit 填占位符即可)
119
+ sillyspec change-rename <旧变更名> <新变更名>
120
+ sillyspec knowledge <search --query "..." --limit N
121
+ | inspect --id "..."
122
+ | validate | refresh
123
+ | propose --title "..." --category <name>>
124
+ sillyspec platform <connect <url> [--token <t>]
125
+ | disconnect
126
+ | sync [--change <name>] | sync-docs [--change <name>]
127
+ | status | pointer [--cleanup]
128
+ | approve <change> | reject <change> [--reason <r>]>
129
+ sillyspec docs migrate
130
+ sillyspec dashboard [--port <N>] [--no-open]
131
+
132
+ 全局选项:
133
+ --json 输出 JSON
134
+ --dir <path> 指定项目目录(默认当前目录)
135
+ --spec-dir <path> 指定规范目录(默认 <项目目录>/.sillyspec)
136
+ --tool <name> 指定工具(init 用)
137
+ --interactive, -i 交互式引导
138
+ --version, -v 查看版本
139
+
140
+ 示例:
141
+ sillyspec init
142
+ sillyspec run brainstorm --change 2026-07-03-add-login
143
+ sillyspec run quick --linked-changes none --done --output "修复手机号校验"
144
+ sillyspec run verify --done --output "验证通过,测试全绿"
145
+ sillyspec run archive --done --confirm --output "归档完成"
146
+ sillyspec run plan --reopen --from-step 2 # 修订 plan,从第 2 步重做
147
+ sillyspec run quick --non-interactive --done --output "CI 内的快修" # 脚本/CI
148
+ sillyspec progress show
149
+ sillyspec worktree apply 2026-07-03-add-login
150
+ `);
151
+ }
152
+
153
+ // --json 模式输出纪律(design §8 风险表):machine-interface 的 runGate/runDerive 执行期间,
154
+ // 被调模块(validators/task-review/verify-postcheck)的人类可读 console.log 会污染 stdout 破坏 JSON。
155
+ // 在整个调用期间把 console.log/info 重定向到 stderr,stdout 留给最终 JSON envelope(D-005@v1)。
156
+ async function withJsonOutput(json, fn) {
157
+ if (!json) return fn();
158
+ const origLog = console.log;
159
+ const origInfo = console.info;
160
+ console.log = (...a) => process.stderr.write(a.map(String).join(' ') + '\n');
161
+ console.info = (...a) => process.stderr.write(a.map(String).join(' ') + '\n');
162
+ try {
163
+ return await fn();
164
+ } finally {
165
+ console.log = origLog;
166
+ console.info = origInfo;
167
+ }
168
+ }
169
+
170
+ async function main() {
171
+ const args = process.argv.slice(2);
172
+
173
+ if (args[0] === '--version' || args[0] === '-v') {
174
+ console.log(getVersion());
175
+ process.exit(0);
176
+ }
177
+
178
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
179
+ printUsage();
180
+ process.exit(0);
181
+ }
182
+
183
+ // E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
184
+ const { ProgressManager, resolvePlatformSpecDir } = await import('./progress.js');
185
+ const { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
186
+
187
+ // 解析全局选项
188
+ let json = false;
189
+ let saveWorkflowRunFlag = false;
190
+ let targetDir = process.cwd();
191
+ let specDir = null;
192
+ let tool = null;
193
+ // --tool 多值收集(逗号分隔 + 重复 flag);空数组 = 未提供,cmdInit 侧自动检测
194
+ const toolValues = [];
195
+ let interactive = false;
196
+ // init 专属:--no-skills 跳过 skills 复制段(platform init 勿污染项目内工具目录)
197
+ let noSkills = false;
198
+ // 平台模式 flag(init 落平台指针用;非 init 命令时忽略——runCommand 自行解析 filteredArgs)
199
+ let platformWorkspaceId = null;
200
+ let platformRuntimeRoot = null;
201
+ const filteredArgs = [];
202
+
203
+ for (let i = 0; i < args.length; i++) {
204
+ if (args[i] === '--json') {
205
+ json = true;
206
+ } else if (args[i] === '--save') {
207
+ saveWorkflowRunFlag = true;
208
+ } else if (args[i] === '--dir' && args[i + 1]) {
209
+ targetDir = resolve(args[i + 1]);
210
+ i++;
211
+ } else if (args[i] === '--spec-dir' && args[i + 1]) {
212
+ specDir = resolve(args[i + 1]);
213
+ i++;
214
+ } else if (args[i] === '--tool' && args[i + 1]) {
215
+ // 多值支持:逗号分隔(--tool claude,codex)与重复 flag(--tool a --tool b)都收集进数组
216
+ for (const v of args[i + 1].split(',')) {
217
+ const t = v.trim();
218
+ if (t) toolValues.push(t);
219
+ }
220
+ i++;
221
+ } else if (args[i] === '--no-skills') {
222
+ // init 专属:跳过 skills 复制段(吞进变量,不透传 filteredArgs)
223
+ noSkills = true;
224
+ } else if (args[i] === '--workspace-id' && args[i + 1]) {
225
+ // 平台模式专属 flag(init 落平台指针用;--runtime-root/--scan-run-id 保持在
226
+ // filteredArgs 由 runCommand 解析,init 侧只需要 workspaceId 作为平台信号 + runtimeRoot)
227
+ platformWorkspaceId = args[i + 1];
228
+ filteredArgs.push(args[i], args[i + 1]);
229
+ i++;
230
+ } else if (args[i] === '--runtime-root' && args[i + 1]) {
231
+ platformRuntimeRoot = resolve(args[i + 1]);
232
+ filteredArgs.push(args[i], args[i + 1]);
233
+ i++;
234
+ } else if (args[i] === '--interactive' || args[i] === '-i') {
235
+ interactive = true;
236
+ } else if (args[i] === '--list' || args[i] === '-l') {
237
+ filteredArgs.push('--list');
238
+ } else {
239
+ filteredArgs.push(args[i]);
240
+ }
241
+ }
242
+
243
+ // F3:--interactive(全局 flag,被上面吞进 interactive 变量)与 --non-interactive(透传到 filteredArgs)
244
+ // 语义相反,同时给是矛盾。在此层检测(command.js 看不到 --interactive)。
245
+ if (interactive && filteredArgs.includes('--non-interactive')) {
246
+ console.error('❌ --non-interactive 与 --interactive 互斥(一个禁用交互、一个强制交互),请只保留一个。');
247
+ process.exit(2);
248
+ }
249
+
250
+ const command = filteredArgs[0];
251
+ // 支持 sillyspec init /path/to/project 语法:如果第二个参数看起来像路径,当作 targetDir
252
+ if (command === 'init' && filteredArgs[1] && !filteredArgs[1].startsWith('-')) {
253
+ targetDir = resolve(filteredArgs[1]);
254
+ filteredArgs.splice(1, 1);
255
+ }
256
+ // ── 自动纠正 cwd ──
257
+ // 当 agent 在 worktree 内跑 pnpm 等工具后 shell cwd 可能被改变,
258
+ // 导致 sillyspec 命令找不到 .sillyspec。此函数尝试从 git root 解析。
259
+ function resolveEffectiveDir(baseDir) {
260
+ if (existsSync(join(baseDir, '.sillyspec'))) return baseDir
261
+ // QUAL-01 收口:原 execSync 字符串拼接(经 shell)→ git-helper 数组形式
262
+ const r = safeGit(baseDir, ['rev-parse', '--show-toplevel'])
263
+ if (r.value && existsSync(join(r.value, '.sillyspec'))) return r.value
264
+ return baseDir
265
+ }
266
+
267
+ const dir = targetDir;
268
+
269
+ if (command === 'init' && !existsSync(dir)) {
270
+ const { mkdirSync } = await import('fs');
271
+ mkdirSync(dir, { recursive: true });
272
+ }
273
+
274
+ if (!existsSync(dir)) {
275
+ console.error(`❌ 目录不存在: ${dir}`);
276
+ process.exit(1);
277
+ }
278
+
279
+ switch (command) {
280
+ case 'init':
281
+ await (await import('./init.js')).cmdInit(dir, { tool, tools: toolValues.length > 0 ? toolValues : null, interactive, specDir, noSkills, platformOpts: (platformWorkspaceId || platformRuntimeRoot) ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot } : null });
282
+ break;
283
+ case 'setup':
284
+ const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
285
+ await (await import('./setup.js')).cmdSetup(dir, { json, list: setupList });
286
+ break;
287
+ case 'progress': {
288
+ const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
289
+ const progDir = specDir ? dir : resolveEffectiveDir(dir);
290
+ const subCommand = filteredArgs[1];
291
+ const stageIdx = filteredArgs.indexOf('--stage');
292
+ const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
293
+ // 解析 --change 参数
294
+ const progChangeIdx = args.indexOf('--change');
295
+ const progChangeName = progChangeIdx >= 0 && args[progChangeIdx + 1] ? args[progChangeIdx + 1] : null;
296
+ // 与 run 入口同源消毒(防路径穿越;progress 下游 completeStage 写 history 文件名拼 change 名)
297
+ if (progChangeName) assertSafeChangeName(progChangeName, '--change 变更名');
298
+
299
+ switch (subCommand) {
300
+ case 'init':
301
+ pm.init(progDir);
302
+ break;
303
+ case 'status':
304
+ case 'show':
305
+ pm.show(progDir, progChangeName);
306
+ break;
307
+ case 'check': {
308
+ const result = pm.checkConsistency(progDir, progChangeName);
309
+ // checkConsistency 只返回 {ok, issues, warnings},不打印——必须在此输出,
310
+ // 否则 agent 跑只读健康检查得到一片寂静,无法分辨「正常」还是「已损坏」。
311
+ if (!result || result.ok) {
312
+ console.log(`✅ 状态一致性检查通过${progChangeName ? `(变更 ${progChangeName})` : '(所有变更)'}`);
313
+ } else {
314
+ console.error(`❌ 状态一致性检查发现问题(${result.issues?.length || 0} 项):`);
315
+ for (const issue of (result.issues || [])) console.error(` - ${issue}`);
316
+ console.error(` 修复:sillyspec progress repair${progChangeName ? ` --change ${progChangeName}` : ''}(先 dry-run 看可修项,加 --apply 执行)`);
317
+ }
318
+ break;
319
+ }
320
+ case 'repair': {
321
+ const repairApply = filteredArgs.includes('--apply');
322
+ pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
323
+ break;
324
+ }
325
+ case 'validate':
326
+ pm.validate(dir, progChangeName);
327
+ break;
328
+ case 'reset':
329
+ pm.reset(dir, stage, progChangeName);
330
+ break;
331
+ case 'set-stage': {
332
+ const setStageName = filteredArgs[2];
333
+ if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage> [--change <name>]'); process.exit(2); }
334
+ pm.setStage(dir, setStageName, progChangeName);
335
+ break;
336
+ }
337
+ case 'add-step': {
338
+ const addStepStage = filteredArgs[2];
339
+ const addStepName = filteredArgs[3];
340
+ if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name> [--change <name>]'); process.exit(2); }
341
+ pm.addStep(dir, addStepStage, addStepName, progChangeName);
342
+ break;
343
+ }
344
+ case 'update-step': {
345
+ const updStepStage = filteredArgs[2];
346
+ const updStepName = filteredArgs[3];
347
+ if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>] [--change <name>]'); process.exit(2); }
348
+ let updStatus = null, updOutput = undefined;
349
+ for (let ai = 0; ai < args.length; ai++) {
350
+ if (args[ai] === '--status' && args[ai + 1]) { updStatus = args[ai + 1]; ai++; }
351
+ if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
352
+ }
353
+ const updForce = args.includes('--force');
354
+ pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput, force: updForce }, progChangeName);
355
+ break;
356
+ }
357
+ case 'complete-stage': {
358
+ const compStageName = filteredArgs[2];
359
+ if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage> [--force]'); process.exit(2); }
360
+ pm.completeStage(dir, compStageName, progChangeName, { force: args.includes('--force') });
361
+ break;
362
+ }
363
+ case 'batch': {
364
+ if (filteredArgs.includes('--status')) {
365
+ const bp = pm.readBatchProgress(dir, progChangeName);
366
+ if (!bp) { console.log('📭 无批量进度数据'); break; }
367
+ const line = pm._renderBatchProgress(bp);
368
+ console.log(line || '📭 无批量进度数据');
369
+ console.log(JSON.stringify(bp, null, 2));
370
+ } else {
371
+ let batchData = {};
372
+ const a = args;
373
+ for (let i = 0; i < a.length; i++) {
374
+ if (a[i] === '--total' && a[i + 1]) { batchData.total = parseInt(a[i + 1]); i++; }
375
+ if (a[i] === '--completed' && a[i + 1]) { batchData.completed = parseInt(a[i + 1]); i++; }
376
+ if (a[i] === '--failed' && a[i + 1]) { batchData.failed = parseInt(a[i + 1]); i++; }
377
+ if (a[i] === '--skipped' && a[i + 1]) { batchData.skipped = parseInt(a[i + 1]); i++; }
378
+ }
379
+ if (Object.keys(batchData).length === 0) {
380
+ console.log('用法: sillyspec progress batch --total 100 --completed 73');
381
+ console.log(' sillyspec progress batch --status');
382
+ process.exit(2);
383
+ }
384
+ pm.updateBatchProgress(dir, batchData, progChangeName);
385
+ console.log('✅ 批量进度已更新');
386
+ }
387
+ break;
388
+ }
389
+ case 'dump': {
390
+ // 只读进度导出(design §6.2):daemon 消费的机器接口 JSON。
391
+ // --json 模式下劫持 console.log 到 stderr,stdout 留给纯 JSON(与 gate/derive 同纪律)。
392
+ const dumpData = pm.dump(progDir);
393
+ if (json) {
394
+ const { buildEnvelope } = await import('./machine-interface.js');
395
+ const envelope = buildEnvelope({
396
+ command: 'progress dump',
397
+ ok: dumpData !== null,
398
+ data: dumpData,
399
+ errors: dumpData === null ? ['无活跃变更或进度数据不存在'] : [],
400
+ });
401
+ console.log(JSON.stringify(envelope));
402
+ } else {
403
+ if (!dumpData) {
404
+ console.log('📭 无活跃变更或进度数据不存在');
405
+ } else {
406
+ console.log(`项目: ${dumpData.project}`);
407
+ console.log(`当前变更: ${dumpData.current_change || '(无)'}`);
408
+ console.log(`当前阶段: ${dumpData.current_stage || '(无)'}`);
409
+ console.log(`最后活跃: ${dumpData.last_active || '(无)'}`);
410
+ console.log(`产物数: ${dumpData.artifacts?.length || 0}`);
411
+ }
412
+ }
413
+ break;
414
+ }
415
+ default:
416
+ console.log('用法: sillyspec progress <init|show|validate|reset|set-stage|add-step|update-step|complete-stage|dump>');
417
+ process.exit(2);
418
+ }
419
+ break;
420
+ }
421
+ case 'gate': {
422
+ // 机器门控(machine-interface v1):聚合「变更的 <stage> 阶段此刻能否标记完成」的综合结论。
423
+ // 只读查询,不依赖 worktree、不写状态(D-002@v1)。
424
+ const gateStage = filteredArgs[1];
425
+ const gateChangeIdx = args.indexOf('--change');
426
+ const gateChange = gateChangeIdx >= 0 && args[gateChangeIdx + 1] ? args[gateChangeIdx + 1] : null;
427
+ if (!gateStage || gateStage.startsWith('-') || !gateChange) {
428
+ console.error('用法: sillyspec gate <stage> --change <name> [--json]\n stage: brainstorm | plan | execute | verify | archive | ...');
429
+ process.exit(2);
430
+ }
431
+ // 与 run 入口同源消毒(防路径穿越;gate 下游拼 marker/changes 路径)
432
+ assertSafeChangeName(gateChange, '--change 变更名');
433
+ const { runGate } = await import('./machine-interface.js');
434
+ // --spec-dir 透传 runGate.specBase(gate CLI 接线修复,P3 坑 3 sillyspec 侧)。
435
+ // runGate 已分离 cwd(跑测试) + specBase(读 local.yaml/spec),但 gate case 之前
436
+ // 只传 cwd 致 --spec-dir 对 gate verify 无效。不传 --spec-dir 时 specBase 走默认
437
+ // resolveSpecDir(cwd),向后兼容。
438
+ const gateOpts = { cwd: dir };
439
+ // A4:平台指针 / 显式 --spec-dir / 本地 fallback 三合一解析 specBase——平台模式(指针指向外部
440
+ // specDir)下 gate 也能核验(原只传 cwd 致 runGate 的 resolveSpecDir 指向本地孤儿库,恒
441
+ // 「无法核验」exit 2)。resolvePlatformSpecDir 指针失效时抛 PointerUnreachableError,由顶层 catch 优雅 fail-closed。
442
+ const gateSpecBase = resolvePlatformSpecDir(dir, specDir);
443
+ if (gateSpecBase) gateOpts.specBase = gateSpecBase;
444
+ // drift 锚定(坑 execute-runs-isolation,对齐 command.js execute drift 守卫 + machine-interface
445
+ // runGate 已扩展的 specDriftAnchor 入参):gate 是顶层命令不走 runCommand/drift 守卫,worktree cwd
446
+ // 下 execute 段 marker 读取会落副本 .runtime。仅在未显式 --spec-dir 时算(对齐守卫条件 !specDir),
447
+ // 命中则 specDriftAnchor 锚主仓——runGate execute 段的 resolveRuntimeRoot 读主仓
448
+ // current-execute-run-id marker,不读 worktree 副本(副本随 cleanup 整目录删消失)。
449
+ if (!specDir) {
450
+ const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
451
+ if (wt) gateOpts.specDriftAnchor = wt.mainSpecBase;
452
+ }
453
+ // W3 task-09:gate 是只读查询命令,best-effort 构造 ctx(D-013)。失败降级 null(不阻断只读命令)——
454
+ // ctx 让 validateTaskReviews/runVerifyTestCheck 正确切跨仓 gitDir;配置错时单仓降级会让两函数
455
+ // 自然报「跨仓 commit 找不到」等错误进入 check.errors,gate 结论仍如实反映问题(D-002@v1 只读契约不破)。
456
+ try {
457
+ const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
458
+ const _ctx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: gateChange, platformOpts: specDir ? { specRoot: specDir } : {} });
459
+ if (_ctx) gateOpts.ctx = _ctx;
460
+ } catch (e) {
461
+ console.warn(`⚠️ gate ctx 构造失败,降级单仓核验(${e.message})`);
462
+ }
463
+ const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
464
+ if (json) {
465
+ process.stdout.write(JSON.stringify(envelope));
466
+ } else {
467
+ const icon = envelope.ok ? '✅' : '❌';
468
+ console.log(`${icon} gate ${gateStage} [${gateChange}]: ${envelope.ok ? '通过' : '未通过'} (exit ${exitCode})`);
469
+ for (const c of (envelope.checks || [])) {
470
+ const ci = c.ok ? '✅' : '❌';
471
+ const tag = c.informational ? ' (informational)' : '';
472
+ console.log(` ${ci} ${c.id}${tag}`);
473
+ for (const e of (c.errors || [])) console.log(` ✗ ${e}`);
474
+ for (const w of (c.warnings || [])) console.log(` ⚠ ${w}`);
475
+ }
476
+ // 顶层 errors/warnings 是各 check 的聚合,已在上面按 check 分组显示;不再重复打印——
477
+ // 非 json 是 agent 默认消费路径,重复文本纯耗 context(W1-J)。
478
+ }
479
+ process.exitCode = exitCode;
480
+ break;
481
+ }
482
+ case 'derive': {
483
+ // 单项事实核验(machine-interface v1):查询变更某一 facet 的真实状态,返回结构化 data。
484
+ const facet = filteredArgs[1];
485
+ const deriveChangeIdx = args.indexOf('--change');
486
+ const deriveChange = deriveChangeIdx >= 0 && args[deriveChangeIdx + 1] ? args[deriveChangeIdx + 1] : null;
487
+ if (!facet || facet.startsWith('-') || !deriveChange) {
488
+ console.error('用法: sillyspec derive <facet> --change <name> [--json]\n facet: execute-evidence | verify-test | task-reviews | artifacts');
489
+ process.exit(2);
490
+ }
491
+ // 与 run 入口同源消毒(防路径穿越;derive 下游拼 marker/changes 路径)
492
+ assertSafeChangeName(deriveChange, '--change 变更名');
493
+ const { runDerive } = await import('./machine-interface.js');
494
+ // --spec-dir 透传 runDerive.specBase(与 gate case 对称,P3 坑 3 sillyspec 侧)。
495
+ // derive verify-test facet 内部同样调 runVerifyTestCheck 依赖 specBase,平台模式
496
+ // 下需透传;不传 --spec-dir 时走默认 resolveSpecDir(cwd),向后兼容。
497
+ const deriveOpts = { cwd: dir };
498
+ // A4:同 gate case——平台指针/--spec-dir/本地 fallback 三合一解析 specBase。
499
+ const deriveSpecBase = resolvePlatformSpecDir(dir, specDir);
500
+ if (deriveSpecBase) deriveOpts.specBase = deriveSpecBase;
501
+ // drift 锚定(同 gate case,对齐 command.js drift 守卫 + machine-interface runDerive 的 specDriftAnchor
502
+ // 入参):derive 顶层命令不走 drift 守卫,worktree cwd 下 task-reviews facet 的 execute-run-id marker
503
+ // 读取会落副本。未显式 --spec-dir 时算 anchor,runDerive task-reviews 的 resolveRuntimeRoot 读主仓 marker。
504
+ if (!specDir) {
505
+ const wt = detectWorktreeSpecDrift(resolveSpecDir(dir));
506
+ if (wt) deriveOpts.specDriftAnchor = wt.mainSpecBase;
507
+ }
508
+ // W3 task-09:derive 同 gate,best-effort 构造 ctx 透传给 verify-test/task-reviews facet(D-013)。
509
+ try {
510
+ const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
511
+ const _ctx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: deriveChange, platformOpts: specDir ? { specRoot: specDir } : {} });
512
+ if (_ctx) deriveOpts.ctx = _ctx;
513
+ } catch (e) {
514
+ console.warn(`⚠️ derive ctx 构造失败,降级单仓核验(${e.message})`);
515
+ }
516
+ const { envelope, exitCode } = await withJsonOutput(json, () => runDerive(facet, deriveChange, deriveOpts));
517
+ if (json) {
518
+ process.stdout.write(JSON.stringify(envelope));
519
+ } else {
520
+ const icon = envelope.ok ? '✅' : '❌';
521
+ console.log(`${icon} derive ${facet} [${deriveChange}]: ${envelope.ok ? '通过' : '未通过'} (exit ${exitCode})`);
522
+ if (envelope.data) console.log(` data: ${JSON.stringify(envelope.data, null, 2)}`);
523
+ for (const e of (envelope.errors || [])) console.log(` ✗ ${e}`);
524
+ for (const w of (envelope.warnings || [])) console.log(` ⚠ ${w}`);
525
+ }
526
+ process.exitCode = exitCode;
527
+ break;
528
+ }
529
+ case 'backfill-reviews': {
530
+ // 手动补 task(reopen execute / 直接实现)缺 review.json 的官方入口(坑 verify-archive-flow-pitfalls 坑2)。
531
+ // 复用 generateTaskReviewDrafts(execute --done 同源兜底,幂等、fail-open):据 git diff base..head + working-tree
532
+ // 按 task allowed_paths 归属生成 cannot_verify 草稿,agent 复核后升级 pass/fail。execute 完成后手动补的 task
533
+ // 无子代理 review 落盘 → archive step1 客观完成度(真相源=review.json verdict)判缺 → 阻断归档;本命令补齐
534
+ // 缺失草稿,独立可随时跑(不必再 execute --done)。缺数据(无 tasks/ 目录 / 改动未 commit / 无 worktree meta)
535
+ // 时 generateTaskReviewDrafts 提前返回 reason,如实打印不报错。
536
+ // --adopt(2026-08-21 agent-手工产出审计项①):已存在 review.json 的 mechanics 字段
537
+ // (schemaVersion/task/base/head/changedFiles/repo)一键重算代填,agent/子代理只写
538
+ // verdict——手算 hash、手抄 diff 清单是纯错误面,gate 校验的恰是这些字段。与草稿互补:
539
+ // 草稿补「缺失」,adopt 修「已存在但 mechanics 错/缺」,verdict 原样保留。
540
+ const brChangeIdx = args.indexOf('--change');
541
+ const brChange = brChangeIdx >= 0 && args[brChangeIdx + 1] ? args[brChangeIdx + 1] : null;
542
+ const brAdopt = args.includes('--adopt');
543
+ if (!brChange) {
544
+ console.error('用法: sillyspec backfill-reviews --change <name> [--adopt] [--json] [--spec-dir <path>]\n 缺失 review.json → 生成 cannot_verify 草稿(解 archive 客观完成度阻断)\n --adopt → 已存在 review.json 的 base/head/changedFiles 等 mechanics 字段一键重算代填(verdict 保留)');
545
+ process.exit(2);
546
+ }
547
+ // 与 run 入口同源消毒(防路径穿越;backfill 下游拼 marker/changes/review 路径)
548
+ assertSafeChangeName(brChange, '--change 变更名');
549
+ const { generateTaskReviewDrafts } = await import('./task-review.js');
550
+ // --spec-dir 透传 platformOpts.specRoot(与 gate/derive 对称);不传走默认 join(cwd,'.sillyspec')。
551
+ const brPlatformOpts = {};
552
+ if (specDir) brPlatformOpts.specRoot = specDir;
553
+ // W3 task-09:best-effort 构造 ctx 透传(D-013),让 task-04 generateTaskReviewDrafts 跨仓 task
554
+ // 用跨仓 gitDir 取 base..head diff。失败降级 null(与 gate/derive 同语义,辅助修复命令不阻断)。
555
+ let _brCtx = null;
556
+ try {
557
+ const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
558
+ _brCtx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: brChange, platformOpts: brPlatformOpts });
559
+ } catch (e) {
560
+ console.warn(`⚠️ backfill-reviews ctx 构造失败,降级单仓草稿(${e.message})`);
561
+ }
562
+ const result = await generateTaskReviewDrafts({ changeName: brChange, cwd: dir, platformOpts: brPlatformOpts, ctx: _brCtx });
563
+ if (json) {
564
+ process.stdout.write(JSON.stringify({ ok: true, command: 'backfill-reviews', change: brChange, ...result }));
565
+ } else {
566
+ if (result.generated > 0) {
567
+ console.log('📄 已补写 ' + result.generated + ' 个 per-task review.json 草稿(cannot_verify,需 agent 复核后升级 pass/fail) [' + brChange + ']');
568
+ } else {
569
+ console.log('ℹ️ 无草稿可补 [' + brChange + ']' + (result.reason ? ':' + result.reason : ''));
570
+ }
571
+ if (result.skipped > 0) {
572
+ console.log(' 跳过 ' + result.skipped + ' 个 task(review.json 已存在 / 空 diff 无从归属)');
573
+ }
574
+ if (result.unattributed && result.unattributed.length > 0) {
575
+ console.warn(' ⚠️ ' + result.unattributed.length + ' 个变更文件未归属任何 task(顺带修复/非源码),草稿未覆盖:' + result.unattributed.join(', '));
576
+ }
577
+ if (result.executeRunId) {
578
+ console.log(' execute run id: ' + result.executeRunId);
579
+ }
580
+ }
581
+ if (brAdopt) {
582
+ const { adoptTaskReviewMechanics } = await import('./task-review.js');
583
+ const adoptResult = await adoptTaskReviewMechanics({ changeName: brChange, cwd: dir, platformOpts: brPlatformOpts, ctx: _brCtx });
584
+ if (json) {
585
+ process.stdout.write(JSON.stringify({ ok: true, command: 'backfill-reviews --adopt', change: brChange, ...adoptResult }));
586
+ } else {
587
+ if (adoptResult.adopted > 0) {
588
+ console.log('🔁 adopt: 重算代填 ' + adoptResult.adopted + ' 个已存在 review.json 的 mechanics 字段(verdict 原样保留) [' + brChange + ']');
589
+ }
590
+ if (adoptResult.unchanged > 0) {
591
+ console.log(' ' + adoptResult.unchanged + ' 个 mechanics 已一致,无需改写');
592
+ }
593
+ if (adoptResult.missing > 0) {
594
+ console.log(' ' + adoptResult.missing + ' 个 task 无 review.json(由上方草稿生成负责,adopt 不覆盖)');
595
+ }
596
+ if (adoptResult.downgraded.length > 0) {
597
+ console.warn(' ⚠️ ' + adoptResult.downgraded.join(', ') + ' 原 verdict 非法/缺失,已降级 cannot_verify(不猜本意,待人工复核升级)');
598
+ }
599
+ if (adoptResult.skipped > 0 || adoptResult.reasons.length > 0) {
600
+ console.log(' 跳过 ' + adoptResult.skipped + ' 个:' + (adoptResult.reasons.join(';') || '原因见上'));
601
+ }
602
+ if (adoptResult.adopted === 0 && adoptResult.unchanged === 0 && adoptResult.skipped === 0 && adoptResult.missing === 0) {
603
+ console.log('ℹ️ adopt 无对象 [' + brChange + ']' + (adoptResult.reasons[0] ? ':' + adoptResult.reasons[0] : ''));
604
+ }
605
+ }
606
+ }
607
+ break;
608
+ }
609
+ case 'workspace': {
610
+ // 多项目工作区管理命令化(2026-08-21 审计第四批 C-1):workspace skill 此前纯手写
611
+ // projects/*.yaml + bash for-loop 探测(skill 明言「CLI 无 workspace 命令」)。
612
+ // 机械面全命令化:add 外科写入(init 三字段 + role/repo 追加,已有值保留)、
613
+ // remove 删登记、status 三态探测表。
614
+ const wsSub = filteredArgs[1];
615
+ const wsArgs = filteredArgs.slice(2);
616
+ const wsFlag = (name) => {
617
+ const i = wsArgs.indexOf(name);
618
+ return i >= 0 && wsArgs[i + 1] ? wsArgs[i + 1] : null;
619
+ };
620
+ const { workspaceAdd, workspaceRemove, workspaceStatus } = await import('./workspace.js');
621
+ if (wsSub === 'add') {
622
+ const name = wsFlag('--name') || wsArgs[0];
623
+ const projPath = wsFlag('--path') || wsArgs[1];
624
+ if (!name || !projPath) {
625
+ console.error('用法: sillyspec workspace add <name> <相对路径> [--role <角色>] [--repo <仓库地址>]\n 登记子项目到 .sillyspec/projects/<name>.yaml(外科写入,已有字段保留;路径须存在)');
626
+ process.exit(2);
627
+ }
628
+ try {
629
+ const r = workspaceAdd({ cwd: dir, name, path: projPath, role: wsFlag('--role'), repo: wsFlag('--repo'), specDir });
630
+ console.log(`✅ ${r.created ? '已登记' : '已更新'}子项目 ${name}: ${r.file}`);
631
+ console.log(` 下一步:cd ${projPath} && sillyspec init(或 scan)`);
632
+ } catch (e) {
633
+ console.error(`❌ ${e.message}`);
634
+ process.exit(1);
635
+ }
636
+ } else if (wsSub === 'remove') {
637
+ const name = wsFlag('--name') || wsArgs[0];
638
+ if (!name) {
639
+ console.error('用法: sillyspec workspace remove <name>\n 删除子项目登记(.sillyspec/projects/<name>.yaml;不删项目本体)');
640
+ process.exit(2);
641
+ }
642
+ try {
643
+ const r = workspaceRemove({ cwd: dir, name, specDir });
644
+ console.log(`🗑️ 已移除子项目登记 ${name}: ${r.file}(项目本体未动)`);
645
+ console.log(' 记得 git add 该删除');
646
+ } catch (e) {
647
+ console.error(`❌ ${e.message}`);
648
+ process.exit(1);
649
+ }
650
+ } else if (!wsSub || wsSub === 'status') {
651
+ const r = workspaceStatus({ cwd: dir, specDir });
652
+ if (json) {
653
+ console.log(JSON.stringify({ command: 'workspace status', ...r }, null, 2));
654
+ break;
655
+ }
656
+ if (r.projects.length === 0) {
657
+ console.log('📭 无子项目登记(.sillyspec/projects/ 为空)——sillyspec workspace add <name> <路径> 登记后管理');
658
+ break;
659
+ }
660
+ console.log(`🏢 工作区(${r.projects.length} 个子项目):`);
661
+ const icon = { scanned: '✅', initialized: '⚠️', unregistered: '❌', missing: '❌' };
662
+ for (const p of r.projects) {
663
+ console.log(` ${icon[p.state] || '•'} ${p.name.padEnd(16)} ${(p.path || '').padEnd(18)} ${(p.role || '').padEnd(14)} ${p.detail}`);
664
+ }
665
+ if (r.sharedDocs.length > 0) {
666
+ console.log(`📄 共享规范:${r.sharedDocs.length} 份(${r.sharedDocs.join('、')})`);
667
+ }
668
+ console.log('💡 操作:workspace add/remove <name> <路径>');
669
+ } else {
670
+ console.error('用法: sillyspec workspace add <name> <路径> [--role] [--repo] | remove <name> | status');
671
+ process.exit(2);
672
+ }
673
+ break;
674
+ }
675
+ case 'scan-fix-leak': {
676
+ // source_root 污染一键修复(2026-08-21 审计第四批 C-3):平台模式 scan-postcheck 对
677
+ // 「agent 把产物写到 <source_root>/.sillyspec/ 而非 specRoot」只报 FAILED 不修,agent 手工
678
+ // 搬常搬错。本命令按检查同款清单搬到 specRoot(已存在不覆盖,报告 skipped)。
679
+ const sflSpecDir = specDir || (await import('./progress.js')).resolvePlatformSpecDir(dir, null);
680
+ if (!sflSpecDir || !existsSync(sflSpecDir)) {
681
+ console.error('❌ 缺 --spec-dir <specRoot>(平台模式 spec 根)——scan-fix-leak 把 <cwd>/.sillyspec/ 误写产物搬到 specRoot');
682
+ process.exit(2);
683
+ }
684
+ const { fixSourceRootLeak } = await import('./scan-postcheck.js');
685
+ const r = fixSourceRootLeak({ cwd: dir, specDir: sflSpecDir });
686
+ if (json) {
687
+ console.log(JSON.stringify({ command: 'scan-fix-leak', ...r }, null, 2));
688
+ break;
689
+ }
690
+ if (r.moved.length === 0 && r.skipped.length === 0) {
691
+ console.log('📭 无污染(<cwd>/.sillyspec/ 下无 docs/projects/workflows/knowledge/manifest/local 产物)');
692
+ break;
693
+ }
694
+ for (const m of r.moved) console.log(`📦 已搬移: ${m}`);
695
+ for (const d of r.removedDirs) console.log(`🧹 已清空目录: ${d}`);
696
+ for (const s of r.skipped) console.log(`⏭️ ${s}`);
697
+ if (r.moved.length > 0) console.log(' 搬完重跑 sillyspec scan 的 postcheck 应转绿');
698
+ break;
699
+ }
700
+ case 'next': {
701
+ // 项目状态探测(2026-08-21 审计第二批 G7/G8):吸收 continue/resume 两个 skill 的
702
+ // 手工探测表(文件存在性状态机),一条命令输出「状态 + 下一步命令 + 依据」。
703
+ // 纯 fs 零 token;活跃进度的权威状态仍归 progress show,本命令管无活跃进度时反推。
704
+ const { detectNextStep } = await import('./run/next.js');
705
+ const nxResult = detectNextStep({ cwd: dir, specDir });
706
+ if (json) {
707
+ console.log(JSON.stringify({ command: 'next', ...nxResult }, null, 2));
708
+ break;
709
+ }
710
+ console.log('🤖 SillySpec 状态探测');
711
+ console.log(` 状态:${nxResult.state}`);
712
+ if (nxResult.activeChanges.length > 0) {
713
+ for (const c of nxResult.activeChanges) {
714
+ console.log(` - ${c.name}(task ${c.checked}/${c.total})`);
715
+ }
716
+ }
717
+ console.log(` 👉 下一步:${nxResult.next}`);
718
+ if (nxResult.evidence.length > 0) {
719
+ console.log(` 依据:${nxResult.evidence.join('、')}`);
720
+ }
721
+ break;
722
+ }
723
+ case 'commit': {
724
+ // 智能提交建议(2026-08-21 审计第二批 G1-G3):commit skill 此前让 agent 手工收集
725
+ // 语义(LAST_COMMIT_TIME / cat QUICKLOG / 扫变更目录 / 按路径模式分类)——进度库、
726
+ // quicklog、git 全在 CLI 手里。一条命令产出统计 + 语义来源 + conventional 建议 +
727
+ // 可照抄的 git 命令。**只建议不执行**(确认权在人,对齐 skill 绝对规则「不要自动提交」)。
728
+ const { collectCommitContext } = await import('./commit-suggest.js');
729
+ const cc = collectCommitContext({ cwd: dir, specDir });
730
+ if (json) {
731
+ console.log(JSON.stringify({ command: 'commit', ...cc }, null, 2));
732
+ break;
733
+ }
734
+ if (!cc.hasChanges) {
735
+ console.log('📭 没有需要提交的修改(工作区干净)');
736
+ break;
737
+ }
738
+ console.log(`📦 待提交变更(${cc.changedCount} 个文件${cc.untrackedCount > 0 ? `,其中未跟踪 ${cc.untrackedCount} 个` : ''};上方 stat 仅列已跟踪文件的改动)`);
739
+ if (cc.stat) console.log(cc.stat.split('\n').map(l => ' ' + l).join('\n'));
740
+ if (cc.quickEntries.length > 0) {
741
+ console.log(`\n📝 QUICKLOG(上次提交 ${cc.lastCommitTime || '(无)'} 之后 ${cc.quickEntries.length} 条):`);
742
+ for (const q of cc.quickEntries) {
743
+ console.log(` ${q.status === '已完成' ? '✅' : '⏳'} ${q.qlId} | ${q.title}${q.status === '已完成' ? '' : '(进行中,不进 commit 语义)'}`);
744
+ }
745
+ }
746
+ if (cc.checkedTasks.length > 0) {
747
+ console.log(`\n✅ execute 已勾 task:`);
748
+ for (const t of cc.checkedTasks) console.log(` - [${t.change}] ${t.item}`);
749
+ }
750
+ if (cc.stageArtifacts.length > 0) {
751
+ console.log(`\n📁 阶段产出:${cc.stageArtifacts.join(' / ')}`);
752
+ }
753
+ if (cc.suggestion) {
754
+ console.log(`\n💡 建议 commit message:`);
755
+ console.log(` ${cc.suggestion.subject}`);
756
+ if (cc.suggestion.body) {
757
+ console.log(cc.suggestion.body.split('\n').map(l => ' ' + l).join('\n'));
758
+ }
759
+ const bodyArgs = cc.suggestion.body ? ` -m "${cc.suggestion.body.replace(/"/g, '\\"').replace(/\n/g, ' ')}"` : '';
760
+ console.log(`\n 执行:git add -A && git commit -m "${cc.suggestion.subject.replace(/"/g, '\\"')}"${bodyArgs}`);
761
+ } else {
762
+ console.log('\n💡 无语义来源匹配——请结合上方 diff stat 手写 message');
763
+ }
764
+ console.log('\n(本命令只建议不提交;确认后执行上面的命令)');
765
+ break;
766
+ }
767
+ case 'verify-probes': {
768
+ // verify 机械探针命令化(2026-08-21 审计第三批 H1/H3/H5/H7/F9):verify-probes.md 模板
769
+ // 六探针中的纯机械四个(未实现标记 grep / 测试文件递归查找 / API 契约对账 / 删除对账)
770
+ // 一条命令跑完并渲染成可直接粘贴的 markdown;半语义探针(2/4 + 3.4/3.5)显式留 TODO。
771
+ // --init 顺带生成 verify-result.md 七章节骨架(探针结果预填;结论留「待填」——gate 找
772
+ // 不到 PASS/FAIL 即判不过,骨架不能直接过门)。已存在不覆盖。
773
+ const vpChangeIdx = args.indexOf('--change');
774
+ const vpChange = vpChangeIdx >= 0 && args[vpChangeIdx + 1] ? args[vpChangeIdx + 1] : null;
775
+ const vpInit = args.includes('--init');
776
+ if (!vpChange) {
777
+ console.error('用法: sillyspec verify-probes --change <name> [--init] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 对账/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖)');
778
+ process.exit(2);
779
+ }
780
+ assertSafeChangeName(vpChange, '--change 变更名');
781
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton } = await import('./verify-probes.js');
782
+ const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir });
783
+ if (json) {
784
+ console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
785
+ break;
786
+ }
787
+ console.log(renderVerifyProbesReport(vpResult));
788
+ if (vpInit) {
789
+ const vpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
790
+ const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
791
+ if (existsSync(vpReportPath)) {
792
+ console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
793
+ } else {
794
+ writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
795
+ console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
796
+ }
797
+ }
798
+ break;
799
+ }
800
+ case 'module-impact': {
801
+ // archive 模块影响矩阵骨架(2026-08-21 审计第三批 D3):文件×模块归属按 _module-map.yaml
802
+ // paths 前缀匹配预填(机械),影响类型/review 标记留 <!--TODO-->(语义)。已存在不覆盖。
803
+ const miChangeIdx = args.indexOf('--change');
804
+ const miChange = miChangeIdx >= 0 && args[miChangeIdx + 1] ? args[miChangeIdx + 1] : null;
805
+ if (!miChange) {
806
+ console.error('用法: sillyspec module-impact --change <name> [--json] [--spec-dir <path>]\n 生成 module-impact.md 骨架(模块影响矩阵按 module-map 预填 + 未匹配文件清单;已存在不覆盖)');
807
+ process.exit(2);
808
+ }
809
+ assertSafeChangeName(miChange, '--change 变更名');
810
+ const { generateModuleImpactSkeleton } = await import('./module-impact.js');
811
+ const miResult = generateModuleImpactSkeleton({ cwd: dir, changeName: miChange, specDir });
812
+ if (!miResult) {
813
+ console.log('ℹ️ 无 module-map 或无 diff 文件可归类——骨架无从生成,agent 全手写(模块索引缺失可先跑 sillyspec modules rebuild)');
814
+ break;
815
+ }
816
+ const miSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
817
+ const miPath = join(miSpecBase, 'changes', miChange, 'module-impact.md');
818
+ if (json) {
819
+ console.log(JSON.stringify({ command: 'module-impact', change: miChange, path: miPath, ...miResult }, null, 2));
820
+ break;
821
+ }
822
+ if (existsSync(miPath)) {
823
+ console.log(`ℹ️ module-impact.md 已存在,不覆盖: ${miPath}`);
824
+ break;
825
+ }
826
+ writeFileSync(miPath, miResult.markdown);
827
+ console.log(`✅ 已生成 module-impact.md 骨架: ${miPath}`);
828
+ console.log(` 归类 ${miResult.matchedCount} 个文件,未匹配 ${miResult.unmatchedCount} 个;影响类型列逐行替换 <!--TODO-->。`);
829
+ break;
830
+ }
831
+ case 'endpoints': {
832
+ // endpoints artifact 生成(2026-08-21 审计第三批 H6):后端 router task 的 endpoints.json
833
+ // 此前靠 agent 手扫装饰器手写(易漏 endpoint)。CLI 复用 endpoint-extractor 全套提取器
834
+ // (FastAPI @router.* / Express router.* / Spring @*Mapping)静态扫描生成,
835
+ // verify 探针 5(verifyApiParity)直接消费该产物。
836
+ const epSub = filteredArgs[1];
837
+ if (epSub !== 'extract') {
838
+ console.error('用法: sillyspec endpoints extract --change <name> [--task task-NN] [--dir <backendRoot> | --files <a.py,b.js...>] [--spec-dir <path>]\n 静态扫描后端路由装饰器生成 endpoints.json(FastAPI/Express/Spring);verify 探针 5 消费');
839
+ process.exit(2);
840
+ }
841
+ const epChangeIdx = args.indexOf('--change');
842
+ const epChange = epChangeIdx >= 0 && args[epChangeIdx + 1] ? args[epChangeIdx + 1] : null;
843
+ const epTaskIdx = args.indexOf('--task');
844
+ const epTask = epTaskIdx >= 0 && args[epTaskIdx + 1] ? args[epTaskIdx + 1] : null;
845
+ const epDirIdx = args.indexOf('--dir');
846
+ const epDir = epDirIdx >= 0 && args[epDirIdx + 1] ? args[epDirIdx + 1] : null;
847
+ const epFilesIdx = args.indexOf('--files');
848
+ const epFilesRaw = epFilesIdx >= 0 && args[epFilesIdx + 1] ? args[epFilesIdx + 1] : null;
849
+ if (!epChange) {
850
+ console.error('❌ 缺 --change <变更名>');
851
+ process.exit(2);
852
+ }
853
+ assertSafeChangeName(epChange, '--change 变更名');
854
+ const { extractFastApiEndpoints, extractExpressEndpoints, extractSpringEndpoints, scanBackendEndpoints } = await import('./endpoint-extractor.js');
855
+ const endpoints = [];
856
+ const epSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
857
+ const epChangeDir = join(epSpecBase, 'changes', epChange);
858
+ if (!existsSync(epChangeDir)) {
859
+ console.error(`❌ 变更目录不存在: ${epChangeDir}`);
860
+ process.exit(1);
861
+ }
862
+ const collectFile = (f) => {
863
+ const abs = resolve(f)
864
+ if (!existsSync(abs)) return
865
+ const ext = extname(abs).toLowerCase()
866
+ try {
867
+ if (ext === '.py') endpoints.push(...extractFastApiEndpoints(abs))
868
+ else if (ext === '.js' || ext === '.ts') endpoints.push(...extractExpressEndpoints(abs))
869
+ else if (ext === '.java') endpoints.push(...extractSpringEndpoints(abs))
870
+ } catch { /* 单文件解析失败不阻断整体 */ }
871
+ }
872
+ if (epDir) {
873
+ endpoints.push(...scanBackendEndpoints(resolve(epDir)))
874
+ } else if (epFilesRaw) {
875
+ for (const f of epFilesRaw.split(',').map(s => s.trim()).filter(Boolean)) collectFile(f)
876
+ } else {
877
+ // 默认扫描面:task 卡 allowed_paths ∪ design §6 清单里的具体源码文件
878
+ const { parseFileChangeListDetailed } = await import('./change-list.js')
879
+ const candidates = new Set()
880
+ if (epTask) {
881
+ const cardPath = join(epChangeDir, 'tasks', `${epTask}.md`)
882
+ if (existsSync(cardPath)) {
883
+ const { parseAllowedPaths } = await import('./stages/plan-postcheck.js')
884
+ for (const p of parseAllowedPaths(readFileSync(cardPath, 'utf8'))) candidates.add(p)
885
+ }
886
+ }
887
+ const designPath = join(epChangeDir, 'design.md')
888
+ if (existsSync(designPath)) {
889
+ for (const e of parseFileChangeListDetailed(designPath)) {
890
+ if (!e.path.startsWith('.sillyspec/') && !/[*?[\]]/.test(e.path)) candidates.add(e.path)
891
+ }
892
+ }
893
+ let scanned = 0
894
+ for (const p of candidates) {
895
+ const abs = join(dir, p)
896
+ if (!existsSync(abs)) continue
897
+ scanned++
898
+ collectFile(p)
899
+ }
900
+ if (candidates.size === 0) {
901
+ console.error('❌ 无扫描面:--task 卡无 allowed_paths 且 design.md 无具体文件清单——显式传 --dir <backendRoot> 或 --files <a.py,b.js>');
902
+ process.exit(1);
903
+ }
904
+ console.log(`ℹ️ 扫描 ${scanned}/${candidates.size} 个清单文件(不存在的跳过)`);
905
+ }
906
+ const artifact = {
907
+ generated_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
908
+ generator: 'sillyspec endpoints extract',
909
+ endpoints: endpoints.map(e => ({ method: e.method, path: e.path, source: e.source })),
910
+ }
911
+ const epRuntimeRoot = (await import('./run/shared.js')).resolveRuntimeRoot({ specRoot: specDir }, epSpecBase);
912
+ const epOutDir = join(epRuntimeRoot, 'contract-artifacts', epChange, epTask || 'scan');
913
+ mkdirSync(epOutDir, { recursive: true });
914
+ const epOutPath = join(epOutDir, 'endpoints.json');
915
+ writeFileSync(epOutPath, JSON.stringify(artifact, null, 2) + '\n');
916
+ console.log(`✅ 提取 ${endpoints.length} 个端点 → ${epOutPath}`);
917
+ console.log(` verify 阶段探针 5(API 契约对账)自动消费该产物;FastAPI/Express/Spring 装饰器均支持。`);
918
+ break;
919
+ }
920
+ case 'scan-fix-headers': {
921
+ // scan 文档 header 幂等补填(2026-08-21 审计第三批 E3):scan-postcheck 对缺
922
+ // author/created_at 只 warning,agent 手改还常忘。本命令补齐(git user + 当前时间),
923
+ // 已有两键的文件不动;有 frontmatter 就地插入,无则补一段。
924
+ const sfhProjectIdx = args.indexOf('--project');
925
+ const sfhProject = sfhProjectIdx >= 0 && args[sfhProjectIdx + 1] ? args[sfhProjectIdx + 1] : null;
926
+ const { fixScanDocHeaders } = await import('./scan-postcheck.js');
927
+ const sfhResult = fixScanDocHeaders({ cwd: dir, specDir, project: sfhProject });
928
+ if (json) {
929
+ console.log(JSON.stringify({ command: 'scan-fix-headers', ...sfhResult }, null, 2));
930
+ break;
931
+ }
932
+ if (sfhResult.fixed.length > 0) {
933
+ for (const f of sfhResult.fixed) console.log(`✅ 已补 header: ${f}`);
934
+ }
935
+ if (sfhResult.skipped.length > 0) console.log(`ℹ️ ${sfhResult.skipped.length} 份已含 author/created_at,未动`);
936
+ if (sfhResult.fixed.length === 0 && sfhResult.skipped.length === 0) console.log('📭 未找到 scan 文档(无 docs/<project>/scan/ 目录)');
937
+ break;
938
+ }
939
+ case 'symbol-impact': {
940
+ // symbol-impact.md 骨架生成(2026-08-21 审计项⑤「报错即生成」的命令入口):gate 此前
941
+ // 只报错不代填 → agent 从零手写整份报告,忘一个 task 就被拦。本命令从 tasks.md(回退
942
+ // plan.md)注册表生成逐 task <!--TODO--> 骨架;gate 拒绝未替换的占位(防骨架直接过门),
943
+ // agent 只需逐行填结论。已存在不覆盖(幂等,agent 产出优先)。
944
+ const siChangeIdx = args.indexOf('--change');
945
+ const siChange = siChangeIdx >= 0 && args[siChangeIdx + 1] ? args[siChangeIdx + 1] : null;
946
+ if (!siChange) {
947
+ console.error('用法: sillyspec symbol-impact --change <name> [--spec-dir <path>]\n 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(已存在不覆盖);gate 拒绝未替换的占位行');
948
+ process.exit(2);
949
+ }
950
+ assertSafeChangeName(siChange, '--change 变更名');
951
+ const siSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
952
+ const siChangeDir = join(siSpecBase, 'changes', siChange);
953
+ const siTasksPath = join(siChangeDir, 'tasks.md');
954
+ const siPlanPath = join(siChangeDir, 'plan.md');
955
+ const siRegistryPath = existsSync(siTasksPath) ? siTasksPath : (existsSync(siPlanPath) ? siPlanPath : null);
956
+ if (!siRegistryPath) {
957
+ console.error(`❌ 变更目录缺 tasks.md/plan.md: ${siChangeDir}`);
958
+ process.exit(1);
959
+ }
960
+ const { generateSymbolImpactSkeleton } = await import('./run/gates.js');
961
+ const siSkeleton = generateSymbolImpactSkeleton(readFileSync(siRegistryPath, 'utf8'));
962
+ if (!siSkeleton) {
963
+ console.error(`❌ 任务清单无 checkbox task 行: ${siRegistryPath}`);
964
+ process.exit(1);
965
+ }
966
+ const siReportPath = join(siChangeDir, 'symbol-impact.md');
967
+ if (existsSync(siReportPath)) {
968
+ console.log(`ℹ️ symbol-impact.md 已存在,不覆盖: ${siReportPath}`);
969
+ break;
970
+ }
971
+ writeFileSync(siReportPath, siSkeleton);
972
+ console.log(`✅ 已生成逐 task 骨架: ${siReportPath}`);
973
+ console.log(' 逐行替换 <!--TODO--> 为结论(无签名级变更也显式写「无」);gate 拒绝未替换的占位行。');
974
+ break;
975
+ }
976
+ case 'register-stage-review': {
977
+ // 手动注册 stage 级 review(brainstorm/plan/execute-acceptance)的确定性 writer ——
978
+ // 生成 run 目录 + review.json 骨架(cannot_verify,待独立审查子代理填 verdict)或 adopt
979
+ // agent 草稿(--from,保留 verdict/checklist 重算 docHash)+ 写 marker。治 tier=independent 时
980
+ // 调度者手动派独立子代理不写 marker 的死锁。docHash 由 CLI computeDocHash 算(部分实现 P6.1b defer)。
981
+ // 纯新增,不改 gate 语义。与 task 级 backfill-reviews 对称(stage 级等价物)。
982
+ const rsrChangeIdx = args.indexOf('--change');
983
+ const rsrChange = rsrChangeIdx >= 0 && args[rsrChangeIdx + 1] ? args[rsrChangeIdx + 1] : null;
984
+ const rsrStageIdx = args.indexOf('--stage');
985
+ const rsrStage = rsrStageIdx >= 0 && args[rsrStageIdx + 1] ? args[rsrStageIdx + 1] : null;
986
+ const rsrFromIdx = args.indexOf('--from');
987
+ const rsrFrom = rsrFromIdx >= 0 && args[rsrFromIdx + 1] ? args[rsrFromIdx + 1] : null;
988
+ // docHash 联动(坑 stage-review-dochash-manual-resync,2026-08-21 实证):改一版 design 要
989
+ // 重算 2-3 个 stage review 的 docHash,手工易漏、gate 报错才补。--refresh-hash 就地刷新单个
990
+ // 既有 review 的 hash(保留结论);--all 一次处理三个 stage(有 review 刷 hash,无 review
991
+ // 生成骨架——改版后一条命令完成全部联动)。
992
+ const rsrRefreshHash = filteredArgs.includes('--refresh-hash');
993
+ const rsrAll = filteredArgs.includes('--all');
994
+ if (!rsrChange || (!rsrStage && !rsrAll) || (rsrAll && rsrStage)) {
995
+ console.error('用法: sillyspec register-stage-review --change <名> (--stage <brainstorm|plan|execute> | --all) [--from <review.json>] [--refresh-hash] [--spec-dir <path>] [--json]\n 生成/adopt/刷新 stage 级 review.json(docHash 自动算 + 写 marker);--all 一次处理三个 stage(有 review 刷新 hash、无 review 生成骨架);--refresh-hash 就地刷新既有 review 的 docHash(保留 verdict 待确认续用)');
996
+ process.exit(2);
997
+ }
998
+ // 与 run 入口同源消毒(防路径穿越;register 下游写 marker、拼 changes/review 路径)
999
+ assertSafeChangeName(rsrChange, '--change 变更名');
1000
+ const { registerStageReview } = await import('./stage-review.js');
1001
+ const rsrPlatformOpts = {};
1002
+ if (specDir) rsrPlatformOpts.specRoot = specDir;
1003
+ const rsrStages = rsrAll ? ['brainstorm', 'plan', 'execute'] : [rsrStage];
1004
+ const rsrResults = [];
1005
+ let rsrFailed = 0;
1006
+ for (const st of rsrStages) {
1007
+ try {
1008
+ // --all 的单 stage 语义:有既有 review → refresh-hash;无 → 骨架。显式 --refresh-hash
1009
+ // 则强制 refresh(无既有 review 时报错,提示先生成骨架)
1010
+ const { getLatestStageReviewRunId, stageReviewMarkerPath } = await import('./stage-review.js');
1011
+ const { resolveRuntimeRoot } = await import('./run/shared.js');
1012
+ const rsrRuntimeRoot = resolveRuntimeRoot(rsrPlatformOpts, rsrPlatformOpts.specRoot || join(dir, '.sillyspec'));
1013
+ const hasExisting = !!getLatestStageReviewRunId(rsrRuntimeRoot, st, rsrChange);
1014
+ const wantRefresh = rsrRefreshHash || (rsrAll && hasExisting);
1015
+ const result = registerStageReview({ changeName: rsrChange, stage: st, fromFile: rsrFrom, cwd: dir, platformOpts: rsrPlatformOpts, refreshHash: wantRefresh });
1016
+ rsrResults.push({ stage: st, ...result });
1017
+ if (json) {
1018
+ process.stdout.write(JSON.stringify({ ok: true, command: 'register-stage-review', change: rsrChange, stage: st, ...result }) + '\n');
1019
+ } else {
1020
+ console.log(`✅ 已注册 ${st} stage review [${result.reviewRunId}] → ${result.reviewPath}(mode: ${result.mode})`);
1021
+ console.log(` marker → ${result.markerPath}`);
1022
+ if (result.mode === 'refreshed') {
1023
+ console.log(` docHash 已按当前 ${result.mainDoc} 重算(verdict 结论是否仍适用需人工确认)`);
1024
+ } else {
1025
+ console.log(` 下一步:独立审查子代理对照 ${result.mainDoc} 填 verdict/checklist 后重跑 --done`);
1026
+ }
1027
+ }
1028
+ } catch (e) {
1029
+ rsrFailed++;
1030
+ if (json) process.stdout.write(JSON.stringify({ ok: false, command: 'register-stage-review', change: rsrChange, stage: st, error: e.message }) + '\n');
1031
+ else console.error(`❌ [${st}] ` + e.message);
1032
+ }
1033
+ }
1034
+ if (rsrFailed > 0) process.exitCode = 1;
1035
+ break;
1036
+ }
1037
+ case 'docs': {
1038
+ const docsSubCmd = filteredArgs[1];
1039
+ if (docsSubCmd === 'migrate') {
1040
+ const { migrateDocs } = await import('./migrate.js');
1041
+ migrateDocs(dir);
1042
+ } else if (docsSubCmd === 'check') {
1043
+ // docs check(2026-08-15 docs-check-productize D-6):文档 file:line 引用校验。
1044
+ // exit code 三档(D-003):0 全绿 / 1 存在无效引用 / 2 配置/IO 错误(DocsCheckConfigError)。
1045
+ // ⚠ flag 解析(F-1 白名单化,docs-signals-o12 D-003 治模式):--json 由全局解析器吞进
1046
+ // json 变量(filteredArgs 不含);分支内 BARE_FLAGS(无值)置位剔除、PAIRED_FLAGS(带值)
1047
+ // 成对剔除;未知 --xxx 显式 exit 2——不再静默落入文档路径误报"不存在"
1048
+ // (历史两次踩坑:--json 恒失效、--suggest 被当文档路径)。
1049
+ // task-03(2026-08-18 platform-map-auto-anchors):白名单加 --fix / --dry-run——
1050
+ // --fix 对 fixable 失效引用自动重锚(消费 task-01 的 inv.fix 分类 + task-02 的 applyFixes
1051
+ // 写回);--dry-run 预览不写盘,单独传(无 --fix)也走预览路径——design §5.2 行为矩阵
1052
+ // --dry-run 列独立存在(「报告修复预览 + exit 1」),即 dryRun 置位 = fix 语义自动生效
1053
+ // 但零写盘(本口径记录于此,防歧义)。
1054
+ const BARE_FLAGS = ['--suggest', '--fix', '--dry-run'];
1055
+ const PAIRED_FLAGS = ['--paths'];
1056
+ const rawDocsArgs = filteredArgs.slice(2);
1057
+ const docsCheckFlags = [];
1058
+ let cliPaths = null;
1059
+ let suggest = false;
1060
+ let fix = false;
1061
+ let dryRun = false;
1062
+ for (let i = 0; i < rawDocsArgs.length; i++) {
1063
+ const a = rawDocsArgs[i];
1064
+ if (a === '--paths' && rawDocsArgs[i + 1] !== undefined) {
1065
+ cliPaths = rawDocsArgs[i + 1].split(',').map(s => s.trim()).filter(Boolean);
1066
+ i++; // 跳过值
1067
+ } else if (a === '--paths') {
1068
+ console.error('❌ docs check: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
1069
+ process.exit(2);
1070
+ } else if (BARE_FLAGS.includes(a)) {
1071
+ if (a === '--suggest') suggest = true;
1072
+ else if (a === '--fix') fix = true;
1073
+ else if (a === '--dry-run') dryRun = true;
1074
+ } else if (a.startsWith('--')) {
1075
+ console.error(`❌ docs check: 未知 flag「${a}」。已知 flag:${[...BARE_FLAGS, ...PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
1076
+ process.exit(2);
1077
+ } else {
1078
+ docsCheckFlags.push(a);
1079
+ }
1080
+ }
1081
+ const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError, applyFixes } = await import('./docs-check.js');
1082
+ try {
1083
+ // 配置优先级:--paths > local.yaml docs-check.paths > 缺省 docs/**/*.md
1084
+ const cfg = readDocsCheckConfig(dir);
1085
+ const result = runDocsCheck({
1086
+ projectRoot: dir,
1087
+ docs: docsCheckFlags.length > 0 ? docsCheckFlags : null,
1088
+ paths: cliPaths || cfg.paths,
1089
+ skip: cfg.skip,
1090
+ keywordAssert: cfg.keywordAssert,
1091
+ crossRepoRoots: cfg.crossRepoRoots,
1092
+ });
1093
+ // task-03 修复链路(FR-04,platform-map-auto-anchors):fixActive 才构造 fixes——
1094
+ // 仅 fix.fixable===true 且 newLine 为整数的条目可重锚;newRef = ref 的行号部分替换为
1095
+ // newLine(文件名原样保留;范围引用 src/a.js:3-8 重锚后收窄为单行语义,design §5.1
1096
+ // 「自动改写为该行号」)。多命中/零命中(needs-manual)只报告不修(D-006),无 --force 逃生口。
1097
+ // applyFixes 空列表零 IO 直返 {0,[]},全绿时调用无副作用;fixes 与 invalid 条目一一对应,
1098
+ // needs-manual 数 = invalid.length - fixes.length。
1099
+ const fixActive = fix || dryRun;
1100
+ const fixes = [];
1101
+ const newRefByInv = new Map();
1102
+ if (fixActive && !result.ok) {
1103
+ for (const inv of result.invalid) {
1104
+ if (inv.fix && inv.fix.fixable === true && Number.isInteger(inv.fix.newLine)) {
1105
+ const newRef = inv.ref.replace(/(\d+)(?:-\d+)?$/, String(inv.fix.newLine));
1106
+ fixes.push({ doc: inv.doc, docLine: inv.docLine, ref: inv.ref, newRef });
1107
+ newRefByInv.set(inv, newRef);
1108
+ }
1109
+ }
1110
+ }
1111
+ const fixResult = fixActive ? applyFixes(dir, fixes, dryRun ? { dryRun: true } : {}) : null;
1112
+ if (json) {
1113
+ // --fix 与 --json 组合保持 stdout 纯 JSON 可解析(constraints):修复统计作 result.fixReport
1114
+ // 附加后整体 stringify,主体仍是 runDocsCheck 返回(invalid[].fix 是 task-01 设计内增量)
1115
+ if (fixResult) result.fixReport = { applied: fixResult.applied, skipped: fixResult.skipped.length, dryRun };
1116
+ console.log(JSON.stringify(result, null, 2));
1117
+ } else {
1118
+ for (const w of result.warnings) console.warn(`⚠️ ${w}`);
1119
+ if (result.crossRepoSkipped > 0) {
1120
+ console.log(`ℹ️ 跨仓 repo:// 引用跳过 ${result.crossRepoSkipped} 处(未配 docs-check.cross_repo_roots 映射;配置后走同款行号+关键词校验)`);
1121
+ }
1122
+ if (result.ok) {
1123
+ console.log(`✅ docs check: ${result.total} 处引用全通过(其中 ${result.kwChecked} 处带关键词断言)`);
1124
+ } else {
1125
+ console.error(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
1126
+ let appliedLeft = fixResult ? fixResult.applied : 0;
1127
+ const skippedQueue = fixResult ? [...fixResult.skipped] : [];
1128
+ for (const inv of result.invalid) {
1129
+ const newRef = newRefByInv.get(inv);
1130
+ if (newRef !== undefined && appliedLeft > 0) {
1131
+ // 已应用(dry-run 为将应用):归因按构造顺序消耗 applied 计数——写回 skipped 仅
1132
+ // 防御路径(文档消失/行内失配)触发,CLI 正常流 fixable 必然落位
1133
+ console.error(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
1134
+ appliedLeft--;
1135
+ } else if (newRef !== undefined) {
1136
+ const s = skippedQueue.shift();
1137
+ console.error(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
1138
+ } else if (fixResult) {
1139
+ // needs-manual:多命中歧义/零命中/无 token——fix.reason 含候选行号,交人工
1140
+ console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
1141
+ } else {
1142
+ console.error(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
1143
+ }
1144
+ if (suggest && inv.suggest && inv.suggest.length > 0) {
1145
+ console.error(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1146
+ }
1147
+ }
1148
+ if (fixResult) {
1149
+ console.error(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1150
+ }
1151
+ console.error(`\n修复指引:行号漂移 → 更新文档行号到当前源码;文件删改名 → 更新引用路径;`);
1152
+ console.error(`关键词缺失但行号正确 → 确认符号是否改名,改文档 token 或行号。`);
1153
+ }
1154
+ }
1155
+ // exit code(design §5.2 行为矩阵逐字对齐):全绿 → 0(--fix 无操作);--fix 后全部
1156
+ // fixable 修复完且无 needs-manual 残留 → 0;仍有 needs-manual 或写回 skipped → 1;
1157
+ // 配置错误 → 2(上方 catch DocsCheckConfigError 维持现状)。判定不重跑 runDocsCheck:
1158
+ // fixable 条目数 === applied 且 needs-manual 数为 0 即全修(定点替换确定性成功,复查无
1159
+ // 增量信息)。无 --fix/--dry-run 时维持 result.ok ? 0 : 1 现状(D-004 逐字节一致)。
1160
+ if (result.ok) {
1161
+ process.exit(0);
1162
+ } else if (fixActive) {
1163
+ const allFixed = fixResult.applied === fixes.length &&
1164
+ fixResult.skipped.length === 0 &&
1165
+ result.invalid.length - fixes.length === 0;
1166
+ process.exit(allFixed ? 0 : 1);
1167
+ } else {
1168
+ process.exit(1);
1169
+ }
1170
+ } catch (e) {
1171
+ if (e instanceof DocsCheckConfigError) {
1172
+ console.error(`❌ docs check 配置错误:${e.message}`);
1173
+ process.exit(2);
1174
+ }
1175
+ throw e;
1176
+ }
1177
+ } else if (docsSubCmd === 'gate') {
1178
+ // docs gate ratchet(docs-signals-o12 cff7479 产品化,pre-push 第三道关):docs check
1179
+ // 失效数 ≤ 基线放行,超基线拦。基线 .sillyspec/docs-check-baseline 首次须显式
1180
+ // --init-baseline 立线(fail-closed,不悄悄合法化存量)。
1181
+ // ⚠ 2026-08-16 补接线:cff7479 落地时漏了本 CLI 分支,.husky/pre-push 一直在跑
1182
+ // usage 输出且 exit 0——gate 形同虚设(docs-signals 范围内文件+hook 已就位,仅此分支缺)。
1183
+ // B9(2026-08-16):flag 白名单化(对齐 docs check 分支)——未知 --xxx 显式 exit 2
1184
+ // (interface-contract §1.3b 宣称),并接线 --paths 透传 runDocsGate.checkOpts(原被忽略)。
1185
+ const GATE_BARE_FLAGS = ['--init-baseline'];
1186
+ const GATE_PAIRED_FLAGS = ['--paths'];
1187
+ const rawGateArgs = filteredArgs.slice(2);
1188
+ let initBaseline = false;
1189
+ let cliGatePaths = null;
1190
+ for (let i = 0; i < rawGateArgs.length; i++) {
1191
+ const a = rawGateArgs[i];
1192
+ if (a === '--paths' && rawGateArgs[i + 1] !== undefined) {
1193
+ cliGatePaths = rawGateArgs[i + 1].split(',').map(s => s.trim()).filter(Boolean);
1194
+ i++;
1195
+ } else if (a === '--paths') {
1196
+ console.error('❌ docs gate: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
1197
+ process.exit(2);
1198
+ } else if (GATE_BARE_FLAGS.includes(a)) {
1199
+ if (a === '--init-baseline') initBaseline = true;
1200
+ } else if (a.startsWith('--')) {
1201
+ console.error(`❌ docs gate: 未知 flag「${a}」。已知 flag:${[...GATE_BARE_FLAGS, ...GATE_PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
1202
+ process.exit(2);
1203
+ } else {
1204
+ console.error(`❌ docs gate: 多余位置参数「${a}」。用法:sillyspec docs gate [--init-baseline] [--paths <glob,...>] [--json]`);
1205
+ process.exit(2);
1206
+ }
1207
+ }
1208
+ const { runDocsGate } = await import('./docs-gate.js');
1209
+ // specBase:平台模式(specDir 指针)优先,本地回退 <repo>/.sillyspec——基线文件锚定
1210
+ // 进度库根(与 .runtime 同级),跨命令稳定。
1211
+ const gateSpecBase = specDir || join(dir, '.sillyspec');
1212
+ const g = await runDocsGate(
1213
+ { projectRoot: dir, specBase: gateSpecBase, initBaseline },
1214
+ cliGatePaths ? { paths: cliGatePaths } : undefined
1215
+ );
1216
+ if (json) {
1217
+ console.log(JSON.stringify(g, null, 2));
1218
+ } else {
1219
+ console.log(g.message);
1220
+ }
1221
+ process.exit(g.exitCode);
1222
+ } else {
1223
+ console.log('用法: sillyspec docs migrate | sillyspec docs check [--paths <glob,...>] [--json] [--suggest] [--fix] [--dry-run] | sillyspec docs gate [--init-baseline] [--json]');
1224
+ process.exit(2);
1225
+ }
1226
+ break;
1227
+ }
1228
+ case 'run': {
1229
+ // doctor --json 等价 doctor 顶层 --json(结构化诊断),与 case 'doctor' 行为一致——
1230
+ // 否则 sillyspec doctor --json 走 runDoctorDiagnostics,sillyspec run doctor --json 走 runCommand
1231
+ // prompt 自检,两者输出不等价(违反"顶层别名 == run <stage>"契约)。
1232
+ if (filteredArgs[1] === 'doctor' && json) {
1233
+ const doctorEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
1234
+ const { runDoctorDiagnostics, formatDoctorJson, writeDoctorDiagnosis } = await import('./doctor-diagnostics.js')
1235
+ const result = await runDoctorDiagnostics({ cwd: doctorEffectiveDir })
1236
+ const output = formatDoctorJson(result, { source_root: dir })
1237
+ const written = writeDoctorDiagnosis(output, result.authoritySpecDir)
1238
+ if (written) console.error(`📁 诊断结果已写入: ${written}`)
1239
+ console.log(JSON.stringify(output, null, 2))
1240
+ process.exitCode = output.overall_status === 'pass' ? 0 : 1
1241
+ break
1242
+ }
1243
+ const { runCommand } = await import('./run.js')
1244
+ // 平台模式(--spec-dir 已指定)时,--dir 是明确的 source_root,不应被 resolveEffectiveDir 纠正
1245
+ const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
1246
+ // 下行 pull:对齐顶层 stage 别名块(task-10 / D-009 / FR-04)。修复前 case 'run' 漏接,
1247
+ // sillyspec run <stage>(含 --done)从不 pull,与别名路径行为分裂(ql-20260818-008)。
1248
+ // 未连接平台静默跳过;本地脏时 pull 内部 skipIfLocalDirty 保守跳过,不覆盖本地进度。
1249
+ const { triggerPullActiveChange } = await import('./run/shared.js')
1250
+ await triggerPullActiveChange(effectiveDir)
1251
+ await runCommand(filteredArgs.slice(1), effectiveDir, specDir, { json })
1252
+ break
1253
+ }
1254
+ // 顶层命令别名:转发 runCommand,与 case 'run': 路径行为一致。
1255
+ // printUsage 宣称所有 stage 都可直接使用,这里补齐全部路由避免落 default 分支。
1256
+ // 注意:filteredArgs[0] === command,直接透传 filteredArgs 即可让 runCommand
1257
+ // 从 args[0] 取到 stage 名。与 case 'run': 的 filteredArgs.slice(1) 区别只在于
1258
+ // slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
1259
+ case 'doctor': {
1260
+ const doctorEffectiveDir = specDir ? dir : resolveEffectiveDir(dir);
1261
+ // 执行流:--cleanup-remnant / --cleanup-ghosts / --dump-db(结构化诊断之外的修复/取证动作)
1262
+ const cleanupRemnant = filteredArgs.includes('--cleanup-remnant');
1263
+ const cleanupGhosts = filteredArgs.includes('--cleanup-ghosts');
1264
+ const dumpDbFlag = filteredArgs.includes('--dump-db');
1265
+ const doctorConfirm = filteredArgs.includes('--confirm');
1266
+ const pathIdx = filteredArgs.indexOf('--path');
1267
+ const dbPath = pathIdx >= 0 && filteredArgs[pathIdx + 1] ? filteredArgs[pathIdx + 1] : null;
1268
+
1269
+ // --align-execute-progress:基于 plan.md 声明对齐 execute 派生戳(仿 --cleanup-remnant 范式)
1270
+ // 默认 dry-run(只报告将补哪些 step),加 --confirm 才写。命中即 break,绝不 fall-through。
1271
+ const alignFlag = filteredArgs.includes('--align-execute-progress');
1272
+ if (alignFlag) {
1273
+ // --change 解析:显式优先,缺省用单活跃变更自动兜底(与 run.js resolveChangeNameAuto 同逻辑)
1274
+ const alignChangeIdx = filteredArgs.indexOf('--change');
1275
+ let alignChange = alignChangeIdx >= 0 && filteredArgs[alignChangeIdx + 1] ? filteredArgs[alignChangeIdx + 1] : null;
1276
+ if (!alignChange) {
1277
+ const changesDir = join(resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec'), 'changes');
1278
+ if (existsSync(changesDir)) {
1279
+ const activeChanges = readdirSync(changesDir, { withFileTypes: true })
1280
+ .filter(e => e.isDirectory() && e.name !== 'archive')
1281
+ .map(e => e.name);
1282
+ if (activeChanges.length === 1) alignChange = activeChanges[0];
1283
+ }
1284
+ }
1285
+ if (!alignChange) {
1286
+ console.error('❌ 无法确定变更名:请用 --change <name>,或确保仅有一个活跃变更');
1287
+ process.exitCode = 2;
1288
+ break;
1289
+ }
1290
+
1291
+ // 复用顶层静态 import(line 12),不动态 await import
1292
+ const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(doctorEffectiveDir, specDir) });
1293
+ const specBase = resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec');
1294
+ // 坑 doctor-align-bypass-review-gate(2026-08-20 实证):align 直写 completed 绕过
1295
+ // completeStep,此前 execute 的 Stage/Task Review Gate 被整体跳过——worktree 清理后的
1296
+ // 恢复场景恰是最需要审计的时刻。confirm 落盘前跑同源只读校验(dry-run 不拦,保持只读语义)。
1297
+ if (doctorConfirm) {
1298
+ const { enforceAlignExecuteReviewGate } = await import('./run/gates.js');
1299
+ const gateBlocked = await enforceAlignExecuteReviewGate({
1300
+ cwd: doctorEffectiveDir, changeName: alignChange, specBase,
1301
+ });
1302
+ if (gateBlocked) {
1303
+ console.error(' 对齐未执行(review 校验未过)。补齐 review 后重跑本命令。');
1304
+ process.exitCode = 1;
1305
+ break;
1306
+ }
1307
+ }
1308
+ let r;
1309
+ try {
1310
+ r = pm.alignExecuteToPlan(doctorEffectiveDir, alignChange, specBase, { confirm: doctorConfirm });
1311
+ } catch (e) {
1312
+ console.error(`❌ 对齐失败:${e.message}`);
1313
+ process.exitCode = 1;
1314
+ break;
1315
+ }
1316
+
1317
+ if (json) {
1318
+ console.log(JSON.stringify(r, null, 2));
1319
+ } else if (r.ok === false) {
1320
+ console.error(`❌ ${r.reason || '对齐未执行'}`);
1321
+ } else {
1322
+ console.log(`✅ 已基于 plan.md 声明对齐 ${r.aligned} 个 step(plan: ${r.planChecked}/${r.planTotal})。`);
1323
+ if (!doctorConfirm) {
1324
+ console.log(` (dry-run:未写盘。加 --confirm 实际落盘,并置 execute 阶段 status=completed。)`);
1325
+ } else {
1326
+ console.log(` 已落盘,execute 阶段 status=completed。请确认 verify 通过。`);
1327
+ }
1328
+ }
1329
+ // ok=false 或 reason 非空 → 1;否则 0(与 --cleanup-remnant 的 r.errors 逻辑同构)
1330
+ process.exitCode = (r.ok === false || r.reason) ? 1 : 0;
1331
+ break;
1332
+ }
1333
+
1334
+ if (cleanupRemnant) {
1335
+ const { cleanupRemnantDbs } = await import('./doctor-diagnostics.js');
1336
+ const r = await cleanupRemnantDbs({ cwd: doctorEffectiveDir, confirm: doctorConfirm });
1337
+ if (json) {
1338
+ console.log(JSON.stringify(r, null, 2));
1339
+ } else {
1340
+ const list = doctorConfirm ? r.deleted : r.would_delete.map((x) => x.path);
1341
+ console.log(`🗑️ 空占位 db ${doctorConfirm ? '已删除' : '待清理(dry-run)'}:${r.count} 个`);
1342
+ for (const p of list) console.log(` ${doctorConfirm ? '✅' : '-'} ${p}`);
1343
+ for (const e of r.errors) console.log(` ❌ ${e.path}: ${e.error}`);
1344
+ if (!doctorConfirm && r.count > 0) console.log(`\n加 --confirm 执行删除(仅删 0 字节占位,不动有内容的 db)。`);
1345
+ }
1346
+ process.exitCode = r.errors.length > 0 ? 1 : 0;
1347
+ break;
1348
+ }
1349
+ if (cleanupGhosts) {
1350
+ // SS-2(2026-08-20):归档幽灵行(db active 无目录)。默认 dry-run,--confirm 才写。
1351
+ // SS-2b:扩空壳目录(active + 目录 0 文件 + last_active 超 7 天),归档行并移除空目录。
1352
+ const { cleanupGhostChanges } = await import('./doctor-diagnostics.js');
1353
+ const r = await cleanupGhostChanges({ cwd: doctorEffectiveDir, specDir, confirm: doctorConfirm });
1354
+ if (json) {
1355
+ console.log(JSON.stringify(r, null, 2));
1356
+ } else {
1357
+ console.log(`👻 幽灵 active 记录(db active 无目录 / 空壳目录超 7 天)${doctorConfirm ? '已归档' : '待清理(dry-run)'}:${r.count} 个 [db: ${r.db_path || '(未找到)'}]`);
1358
+ for (const n of r.ghosts) console.log(` ${doctorConfirm ? '✅' : '-'} ${n}`);
1359
+ for (const n of (r.empty_shells || [])) console.log(` ${doctorConfirm ? '✅' : '🫙'} ${n}(空壳目录${doctorConfirm ? ',空目录已移除' : ''})`);
1360
+ for (const n of (r.skipped_nonempty || [])) console.log(` ⏭️ ${n}(复查时目录已非空,已放过——可能是活跃变更)`);
1361
+ for (const e of r.errors) console.log(` ❌ ${e.name || ''}: ${e.error}`);
1362
+ if (!doctorConfirm && r.count > 0) console.log(`
1363
+ 加 --confirm 执行归档(仅改 status=archived,可逆;空壳同时移除 0 文件空目录,不删任何有内容的目录)。`);
1364
+ if (r.reason) console.log(`ℹ️ ${r.reason}`);
1365
+ }
1366
+ process.exitCode = r.errors.length > 0 ? 1 : 0;
1367
+ break;
1368
+ }
1369
+ if (dumpDbFlag) {
1370
+ if (!dbPath) { console.error('❌ --dump-db 需要 --path <db 路径>'); process.exitCode = 2; break; }
1371
+ const { dumpDb } = await import('./doctor-diagnostics.js');
1372
+ const r = await dumpDb({ dbPath, cwd: doctorEffectiveDir });
1373
+ if (json) {
1374
+ console.log(JSON.stringify(r, null, 2));
1375
+ } else if (r.ok === false && r.error) {
1376
+ console.error(`❌ ${r.error}`);
1377
+ } else {
1378
+ console.log(`📦 dump ${dbPath} (${r.meta?.size}B):${r.changes?.length || 0} changes, ${r.stages?.length || 0} stages`);
1379
+ if (r.written_to) console.log(` 写入:${r.written_to}`);
1380
+ }
1381
+ process.exitCode = r.ok === false ? 1 : 0;
1382
+ break;
1383
+ }
1384
+ // --json:走结构化诊断(doctor-diagnostics.js),输出 JSON + 落盘 doctor-diagnosis.json
1385
+ if (json) {
1386
+ const { runDoctorDiagnostics, formatDoctorJson, writeDoctorDiagnosis } = await import('./doctor-diagnostics.js');
1387
+ const result = await runDoctorDiagnostics({ cwd: doctorEffectiveDir });
1388
+ const output = formatDoctorJson(result, { source_root: dir });
1389
+ const written = writeDoctorDiagnosis(output, result.authoritySpecDir);
1390
+ if (written) console.error(`📁 诊断结果已写入: ${written}`);
1391
+ console.log(JSON.stringify(output, null, 2));
1392
+ process.exitCode = output.overall_status === 'pass' ? 0 : 1;
1393
+ break;
1394
+ }
1395
+ // 否则:保持原有 prompt 驱动的 bash 自检流程
1396
+ const { runCommand } = await import('./run.js');
1397
+ await runCommand([command, ...filteredArgs.slice(1)], doctorEffectiveDir, specDir, { json });
1398
+ break;
1399
+ }
1400
+ case 'scan': {
1401
+ // scan diff 子命令(D-001@v1:接线唯一入口 index.js,非 command.js 裸 token 静默吞)。
1402
+ // 纯只读比较命令:跳过下方共享块的 triggerPullActiveChange(不触发网络 pull),
1403
+ // 也不走 scan 主流程(--standard/--deep 分支不动)。解析 --base/--full/--report 后转发 scan-diff。
1404
+ if (filteredArgs[1] === 'diff') {
1405
+ const { runScanDiff } = await import('./scan-diff.js')
1406
+ const diffBaseIdx = filteredArgs.indexOf('--base')
1407
+ const diffBase = diffBaseIdx >= 0 && filteredArgs[diffBaseIdx + 1] ? filteredArgs[diffBaseIdx + 1] : null
1408
+ // specBase/projectName 与 scan 主流程同口径(scan-profile.js:specBase=进度库根,projectName=仓库根 basename)
1409
+ const diffEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
1410
+ process.exitCode = runScanDiff({
1411
+ projectRoot: diffEffectiveDir,
1412
+ specBase: specDir || join(diffEffectiveDir, '.sillyspec'),
1413
+ projectName: basename(diffEffectiveDir),
1414
+ base: diffBase,
1415
+ full: filteredArgs.includes('--full'),
1416
+ report: filteredArgs.includes('--report'),
1417
+ })
1418
+ break
1419
+ }
1420
+ }
1421
+ case 'status':
1422
+ case 'quick':
1423
+ case 'explore':
1424
+ case 'brainstorm':
1425
+ case 'plan':
1426
+ case 'execute':
1427
+ case 'verify':
1428
+ case 'auto':
1429
+ case 'archive': {
1430
+ const { runCommand } = await import('./run.js')
1431
+ const stageArgs = [command, ...filteredArgs.slice(1)]
1432
+ const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
1433
+ // 下行 pull:stage 命令(run/--done,含 archive)启动时拉一次(task-10 / D-009 / FR-04)
1434
+ // 不在每步 pull,仅低频边界点;未连接平台静默跳过
1435
+ const { triggerPullActiveChange } = await import('./run/shared.js')
1436
+ await triggerPullActiveChange(effectiveDir)
1437
+ await runCommand(stageArgs, effectiveDir, specDir, { json })
1438
+ break
1439
+ }
1440
+ case 'knowledge': {
1441
+ const { cmdKnowledge } = await import('./stages/knowledge.js')
1442
+ await cmdKnowledge(filteredArgs.slice(1), specDir ? dir : resolveEffectiveDir(dir), { specDir })
1443
+ break
1444
+ }
1445
+ case 'dashboard': {
1446
+ // Parse dashboard options
1447
+ let port = 3456;
1448
+ let openBrowser = true;
1449
+
1450
+ for (let i = 1; i < args.length; i++) {
1451
+ if (args[i] === '--port' && args[i + 1]) {
1452
+ port = parseInt(args[i + 1], 10);
1453
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1454
+ console.error(`❌ 无效端口: ${args[i + 1]}(应为 1-65535 整数)`);
1455
+ process.exit(2);
1456
+ }
1457
+ i++;
1458
+ } else if (args[i] === '--no-open') {
1459
+ openBrowser = false;
1460
+ }
1461
+ }
1462
+
1463
+ // Import and start dashboard server
1464
+ const { startServer } = await import('../packages/dashboard/server/index.js');
1465
+ startServer({ port, open: openBrowser });
1466
+
1467
+ // Keep process alive
1468
+ console.log('按 Ctrl+C 停止服务器');
1469
+ break;
1470
+ }
1471
+ case 'worktree': {
1472
+ const { WorktreeManager } = await import('./worktree.js');
1473
+ const { ProgressManager } = await import('./progress.js');
1474
+ const wtSubCmd = filteredArgs[1];
1475
+ const wtName = filteredArgs.slice(2).find(a => !a.startsWith('-'));
1476
+ const wm = new WorktreeManager({ cwd: dir });
1477
+ const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
1478
+
1479
+ // isolation 写入 DB 的辅助函数
1480
+ function _writeIsolationToDB(cwd, changeName, info) {
1481
+ if (info.blocked) {
1482
+ pm.updateChangeIsolation(cwd, changeName, {
1483
+ status: 'blocked',
1484
+ mode: null,
1485
+ reason: info.reason,
1486
+ });
1487
+ } else {
1488
+ const mode = info.mode || 'worktree';
1489
+ const statusMap = { 'worktree': 'verified', 'native-worktree': 'verified', 'in-place-fallback': 'degraded' };
1490
+ pm.updateChangeIsolation(cwd, changeName, {
1491
+ status: statusMap[mode] || 'verified',
1492
+ mode,
1493
+ });
1494
+ }
1495
+ }
1496
+
1497
+ if (!wtSubCmd || wtSubCmd === 'help' || wtSubCmd === '--help' || wtSubCmd === '-h') {
1498
+ console.log(`
1499
+ SillySpec worktree — git worktree 隔离管理
1500
+
1501
+ 用法:
1502
+ sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
1503
+ sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] 校验并应用变更到主工作区
1504
+ sillyspec worktree assess <change-name> 风险审计 + 自动 apply
1505
+ sillyspec worktree diff <change-name> [--base <commit>] 查看 worktree 相对 base 的变更
1506
+ sillyspec worktree list 列出所有活跃 worktree
1507
+ sillyspec worktree meta <change-name> 读取 worktree meta.json
1508
+ sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
1509
+ sillyspec worktree doctor [--fix] [--stale-hours N] [--change <name>] 健康检查 + 修复(--change 仅扫指定 change)
1510
+
1511
+ 选项:
1512
+ --base <branch> create: 指定基础分支(默认当前 HEAD)
1513
+ --check-only apply: 只输出检查结果,不实际 apply
1514
+ `);
1515
+ break;
1516
+ }
1517
+
1518
+ switch (wtSubCmd) {
1519
+ case 'create': {
1520
+ if (!wtName) {
1521
+ console.error('❌ 用法: sillyspec worktree create <change-name> [--base <branch>]');
1522
+ process.exit(1);
1523
+ }
1524
+ // F6 路径穿越消毒:worktree 名用于分支名 + worktree 目录,含 ../ 会逃出 .sillyspec/worktrees/。
1525
+ try { assertSafeChangeName(wtName, 'worktree 变更名'); }
1526
+ catch (e) { console.error(`❌ ${e.message}`); process.exit(2); }
1527
+ const baseIdx = args.indexOf('--base');
1528
+ const base = baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1] : undefined;
1529
+ try {
1530
+ const info = wm.create(wtName, { base });
1531
+ console.log(`✅ worktree 已创建`);
1532
+ console.log(` 分支: ${info.branch}`);
1533
+ console.log(` 路径: ${info.worktreePath}`);
1534
+ console.log(` 基准: ${info.baseHash.slice(0, 8)}`);
1535
+ if (info.mode) {
1536
+ console.log(` 模式: ${info.mode}`);
1537
+ }
1538
+ // 写入 isolation 信息到 sillyspec.db(_writeIsolationToDB)
1539
+ await _writeIsolationToDB(dir, wtName, info);
1540
+ } catch (e) {
1541
+ console.error(`❌ ${e.message}`);
1542
+ // 写入 blocked 状态到 sillyspec.db(_writeIsolationToDB)
1543
+ await _writeIsolationToDB(dir, wtName, { blocked: true, reason: e.message });
1544
+ process.exit(1);
1545
+ }
1546
+ break;
1547
+ }
1548
+ case 'apply': {
1549
+ if (!wtName) {
1550
+ console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap]');
1551
+ process.exit(1);
1552
+ }
1553
+ const checkOnly = args.includes('--check-only');
1554
+ const merge = args.includes('--merge');
1555
+ const skipOverlap = args.includes('--skip-overlap');
1556
+
1557
+ // 解析 --base 参数(默认 merge-base)
1558
+ let base = 'merge-base';
1559
+ const baseIdx = args.indexOf('--base');
1560
+ if (baseIdx !== -1) {
1561
+ const baseVal = args[baseIdx + 1];
1562
+ if (!baseVal || (!['merge-base', 'baseline'].includes(baseVal))) {
1563
+ console.error('❌ --base 参数值非法,必须是 merge-base 或 baseline');
1564
+ process.exit(1);
1565
+ }
1566
+ base = baseVal;
1567
+ }
1568
+
1569
+ const { applyWorktree, withMainRepoLock } = await import('./worktree-apply.js');
1570
+ // W3 task-09:apply 链路构造 ctx(D-013),让 task-05 applyWorktree 按 ctx 区分主仓 A5 / 跨仓 no-op。
1571
+ // 跨仓 apply=no-op(G1 D-009):校验 review.head 是跨仓真实 commit + 跳过 wm.cleanup。
1572
+ // ctx 构造 fail-closed(约束②)——跨仓配置错时 apply 必须阻断(走错仓=数据所有权事故),不降级。
1573
+ let _applyCtx = null;
1574
+ try {
1575
+ const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
1576
+ _applyCtx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: wtName, platformOpts: specDir ? { specRoot: specDir } : {} });
1577
+ } catch (e) {
1578
+ console.error(`❌ apply 失败:跨仓 MultiRepoContext 构造失败(${e.message})`);
1579
+ process.exit(1);
1580
+ }
1581
+ // 主仓 apply 互斥锁(坑 main-apply-no-mutex):真 apply(非 checkOnly)在锁内跑——
1582
+ // apply 链(rollback/merge/cleanup)直接改主仓工作区,两会话并发互踩会互相清文件。
1583
+ let result;
1584
+ try {
1585
+ result = checkOnly
1586
+ ? applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap })
1587
+ : await withMainRepoLock(dir, wtName, 'apply', () => applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap }));
1588
+ } catch (lockErr) {
1589
+ console.error(`❌ ${lockErr.message}`);
1590
+ process.exit(1);
1591
+ }
1592
+
1593
+ if (result.errors.length > 0) {
1594
+ console.error(`❌ 校验失败:`);
1595
+ for (const err of result.errors) {
1596
+ console.error(` ${err}`);
1597
+ }
1598
+ if (result.rescueCommands) {
1599
+ console.error('');
1600
+ console.error(`🆘 Rescue commands (${result.rescueCommands.cpFileCount} safe / ${result.rescueCommands.excludedCount} excluded,旁路 git apply,cp 后需手动 sillyspec worktree cleanup ${wtName}):`);
1601
+ for (const c of result.rescueCommands.commands) console.error(` ${c}`);
1602
+ for (const w of result.rescueCommands.warnings) console.error(` ⚠️ ${w}`);
1603
+ }
1604
+ process.exit(1);
1605
+ }
1606
+
1607
+ if (result.changedFiles.length === 0) {
1608
+ console.log('📭 无变更需要应用');
1609
+ break;
1610
+ }
1611
+
1612
+ if (checkOnly) {
1613
+ console.log(`✅ 检查通过 (${result.changedFiles.length} 个文件):`);
1614
+ for (const f of result.changedFiles) {
1615
+ console.log(` ${f}`);
1616
+ }
1617
+ } else if (result.merged) {
1618
+ console.log(`✅ 已通过 git merge 应用变更(baseline 漂移降级,引入合并提交)${result.mergeSummary ? ':' + result.mergeSummary : ''}`);
1619
+ } else {
1620
+ console.log(`✅ 已应用 ${result.changedFiles.length} 个文件变更:`);
1621
+ for (const f of result.changedFiles) {
1622
+ console.log(` ${f}`);
1623
+ }
1624
+ // filterDeliverableFiles 精细化排除(保留 .sillyspec/docs/,排 changes/+.runtime/+quicklog/ + meta.json):
1625
+ // 模块文档(.sillyspec/docs/)= 交付物,会 auto-apply 到主工作区;变更文档/运行时/quicklog 不 apply。
1626
+ // 须告知 agent 哪些未落地,否则它期望这些文件落地却找不到(memory: worktree-apply-excludes-module-docs)。
1627
+ console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从 worktree 分支或 dangling commit 取(git show sillyspec/${wtName}:<path>)。`);
1628
+ // 提交复用 pathspec(坑 apply-commit-pathspec-sweep):主仓无关未提交文件易被目录级
1629
+ // git add 误扫——给精确清单,勿用 git add <目录>/ 全扫
1630
+ if (Array.isArray(result.commitPathspec) && result.commitPathspec.length > 0 && result.pathspecFile) {
1631
+ console.log(`\n📌 提交时用本变更精确 pathspec(勿目录级 add 误扫无关文件):`);
1632
+ if (result.commitPathspec.length <= 30) {
1633
+ console.log(` git add -- ${result.commitPathspec.join(' ')}`);
1634
+ } else {
1635
+ console.log(` git add --pathspec-from-file="${result.pathspecFile}"`);
1636
+ }
1637
+ console.log(` (完整清单 ${result.commitPathspec.length} 个文件已存 ${result.pathspecFile},可 --pathspec-from-file 复用)`);
1638
+ }
1639
+ }
1640
+ if (result.warnings && result.warnings.length > 0) {
1641
+ for (const w of result.warnings) {
1642
+ console.log(`⚠️ ${w}`);
1643
+ }
1644
+ }
1645
+ break;
1646
+ }
1647
+ case 'assess': {
1648
+ if (!wtName) {
1649
+ console.error('❌ 用法: sillyspec worktree assess <change-name>');
1650
+ process.exit(1);
1651
+ }
1652
+ const { assessApplyRisk } = await import('./worktree-apply.js');
1653
+ const assessment = assessApplyRisk(wtName, { cwd: dir });
1654
+
1655
+ // W3 task-09:assess 自动 apply 路径构造 ctx(D-013),SAFE/WARNING 时透传给 applyWorktree。
1656
+ // 构造 fail-closed(跨仓配置错阻断,与显式 apply case 同语义)。
1657
+ let _assessCtx = null;
1658
+ try {
1659
+ const { getOrCreateMultiRepoContext } = await import('./run/shared.js');
1660
+ _assessCtx = await getOrCreateMultiRepoContext({ cwd: dir, changeName: wtName, platformOpts: specDir ? { specRoot: specDir } : {} });
1661
+ } catch (e) {
1662
+ if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
1663
+ console.error(`❌ assess 自动 apply 失败:跨仓 MultiRepoContext 构造失败(${e.message})`);
1664
+ process.exit(1);
1665
+ }
1666
+ // blocked 决策不触发 apply,ctx 失败仅记 warn(不阻断只读 assess 展示)
1667
+ console.warn(`⚠️ ctx 构造失败(${e.message})`);
1668
+ }
1669
+ const SEPARATOR = '─'.repeat(32);
1670
+ console.log('Worktree Apply Decision');
1671
+ console.log(SEPARATOR);
1672
+ const decisionIcon = assessment.decision === 'SAFE' ? '✅' : assessment.decision === 'WARNING' ? '⚠️ ' : '🚫';
1673
+ console.log(`Decision: ${decisionIcon} ${assessment.decision}`);
1674
+ console.log(`Changed files: ${assessment.changedFiles.length}`);
1675
+ if (assessment.stats.additions > 0 || assessment.stats.deletions > 0) {
1676
+ console.log(`Additions: +${assessment.stats.additions} Deletions: -${assessment.stats.deletions}`);
1677
+ }
1678
+
1679
+ if (assessment.reasons.length > 0) {
1680
+ console.log('');
1681
+ console.log('Blocked reasons:');
1682
+ for (const r of assessment.reasons) r.split('\n').forEach(l => console.log(` ${l}`));
1683
+ }
1684
+ if (assessment.warnings.length > 0) {
1685
+ console.log('');
1686
+ console.log('Warnings:');
1687
+ for (const w of assessment.warnings) console.log(` ⚠️ ${w}`);
1688
+ }
1689
+
1690
+ if (assessment.rescueCommands) {
1691
+ console.log('');
1692
+ console.log(`🆘 Rescue commands (${assessment.rescueCommands.cpFileCount} safe / ${assessment.rescueCommands.excludedCount} excluded,旁路 git apply,cp 后需手动 sillyspec worktree cleanup ${wtName}):`);
1693
+ for (const c of assessment.rescueCommands.commands) console.log(` ${c}`);
1694
+ for (const w of assessment.rescueCommands.warnings) console.log(` ⚠️ ${w}`);
1695
+ }
1696
+ console.log('');
1697
+ if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
1698
+ console.log('Action: auto-applying...');
1699
+ const { applyWorktree, withMainRepoLock } = await import('./worktree-apply.js');
1700
+ // 主仓互斥锁(与手动 apply 同款):自动 apply 同样改主仓工作区,必须互斥
1701
+ let applyResult;
1702
+ try {
1703
+ applyResult = await withMainRepoLock(dir, wtName, 'assess-auto-apply', () => applyWorktree(wtName, { cwd: dir, ctx: _assessCtx }));
1704
+ } catch (lockErr) {
1705
+ console.error(`❌ 自动 apply 未执行:${lockErr.message}`);
1706
+ break;
1707
+ }
1708
+ if (applyResult.errors.length > 0) {
1709
+ console.error('❌ apply 失败:', applyResult.errors.join('; '));
1710
+ } else {
1711
+ console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更:`);
1712
+ for (const f of applyResult.changedFiles) console.log(` ${f}`);
1713
+ console.log(` ℹ️ 注:.sillyspec/changes、.sillyspec/.runtime、.sillyspec/quicklog 下的文件按规则不自动 apply(模块文档 .sillyspec/docs/ 会自动 apply),如需请手动从分支取(git show sillyspec/${wtName}:<path>)。`);
1714
+ }
1715
+ } else {
1716
+ console.log('Action: blocked');
1717
+ console.log(' → 检查变更: sillyspec worktree diff ' + wtName);
1718
+ console.log(' → 丢弃变更: sillyspec worktree cleanup ' + wtName);
1719
+ console.log(' → baseline 漂移降级: sillyspec worktree apply ' + wtName + ' --merge');
1720
+ }
1721
+ break;
1722
+ }
1723
+ case 'meta': {
1724
+ if (!wtName) {
1725
+ console.error('❌ 用法: sillyspec worktree meta <change-name>');
1726
+ process.exit(1);
1727
+ }
1728
+ const meta = wm.getMeta(wtName);
1729
+ if (!meta) {
1730
+ console.error(`❌ 未找到 worktree meta: ${wtName}(可能未创建或已被清理)`);
1731
+ process.exit(1);
1732
+ }
1733
+ console.log(`change: ${wtName}`);
1734
+ console.log(`worktreePath: ${meta.worktreePath || '(未设置)'}`);
1735
+ console.log(`branch: ${meta.branch || '(未设置)'}`);
1736
+ console.log(`mode: ${meta.mode || '(未设置)'}`);
1737
+ if (meta.baseBranch) console.log(`baseBranch: ${meta.baseBranch}`);
1738
+ if (meta.baseHash) console.log(`baseHash: ${String(meta.baseHash).slice(0, 8)}`);
1739
+ console.log('');
1740
+ console.log('JSON:');
1741
+ console.log(JSON.stringify(meta, null, 2));
1742
+ break;
1743
+ }
1744
+ case 'diff': {
1745
+ if (!wtName) {
1746
+ console.error('❌ 用法: sillyspec worktree diff <change-name> [--base <commit>]');
1747
+ process.exit(1);
1748
+ }
1749
+ const meta = wm.getMeta(wtName);
1750
+ if (!meta) {
1751
+ console.error(`❌ 未找到 worktree meta: ${wtName}(可能未创建或已被清理)`);
1752
+ process.exit(1);
1753
+ }
1754
+ if (!meta.worktreePath || !existsSync(meta.worktreePath)) {
1755
+ console.error(`❌ worktree 目录不存在: ${meta.worktreePath || '(未设置)'}(可能已被清理,可跑 sillyspec worktree doctor)`);
1756
+ process.exit(1);
1757
+ }
1758
+ // base 优先级:显式 --base > meta.baseHash(create 时锚的主仓 HEAD)> 当前 HEAD。
1759
+ // diff 比较 worktree 工作区(含未提交)vs base,与 _changesAlreadyOnMain 同口径。
1760
+ const baseIdx = filteredArgs.indexOf('--base');
1761
+ const explicitBase = baseIdx >= 0 ? filteredArgs[baseIdx + 1] : null;
1762
+ const base = explicitBase || meta.baseHash || 'HEAD';
1763
+ let out = '';
1764
+ try {
1765
+ // base 作为独立 argv 元素传入,不经 shell 拆词/注入;worktreePath 经 cwd,不插值。
1766
+ // trim:false 保留 git diff 原始输出(含尾换行)供 stdout.write;空检查靠临时 .trim()。
1767
+ // timeout 30s:diff 属潜在长操作(对齐 design R3 与 verify-postcheck 口径)。
1768
+ out = git(meta.worktreePath, ['--no-pager', 'diff', '--no-renames', base], { trim: false, timeout: 30000 });
1769
+ } catch (e) {
1770
+ console.error(`❌ git diff 失败(base=${String(base).slice(0, 8)}): ${e.message}`);
1771
+ process.exit(1);
1772
+ }
1773
+ if (!out.trim()) {
1774
+ console.log(`(无变更:worktree 工作区与 base ${String(base).slice(0, 8)} 一致)`);
1775
+ } else {
1776
+ process.stdout.write(out);
1777
+ }
1778
+ break;
1779
+ }
1780
+ case 'list': {
1781
+ const items = wm.list();
1782
+ if (items.length === 0) {
1783
+ console.log('📭 无活跃 worktree');
1784
+ break;
1785
+ }
1786
+ // 计算列宽
1787
+ const maxName = Math.max('Change Name'.length, ...items.map(i => i.changeName.length));
1788
+ const maxBranch = Math.max('Branch'.length, ...items.map(i => i.branch.length));
1789
+ const header = ` ${'Change Name'.padEnd(maxName)} ${'Branch'.padEnd(maxBranch)} Created`;
1790
+ const sep = ` ${'─'.repeat(maxName)} ${'─'.repeat(maxBranch)} ${'─'.repeat(19)}`;
1791
+ console.log(header);
1792
+ console.log(sep);
1793
+ for (const item of items) {
1794
+ const created = item.createdAt ? item.createdAt.replace('T', ' ').replace('Z', '').slice(0, 19) : '-';
1795
+ console.log(` ${item.changeName.padEnd(maxName)} ${item.branch.padEnd(maxBranch)} ${created}`);
1796
+ }
1797
+ break;
1798
+ }
1799
+ case 'cleanup': {
1800
+ if (!wtName) {
1801
+ console.error('❌ 用法: sillyspec worktree cleanup <change-name>');
1802
+ process.exit(1);
1803
+ }
1804
+ const forceFlag = args.includes('--force');
1805
+ try {
1806
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):cleanup 删 worktree 注册表/分支/目录,
1807
+ // 与并行会话的 apply/cleanup 互踩会互相清——与其他写主仓操作共用一把 main-repo.lock
1808
+ const { withMainRepoLock } = await import('./worktree-apply.js');
1809
+ const result = await withMainRepoLock(dir, wtName, 'worktree-cleanup', () => wm.cleanup(wtName, { force: forceFlag }));
1810
+ if (result.result === 'cleaned' || result.result === 'force-cleaned') {
1811
+ console.log(`✅ worktree 已清理: ${wtName} (mode: ${result.mode})`);
1812
+ if (result.details?.length > 0) {
1813
+ for (const d of result.details) {
1814
+ if (d.startsWith('⚠️')) console.log(` ${d}`);
1815
+ }
1816
+ }
1817
+ } else if (result.result === 'skipped') {
1818
+ console.log(`⏭️ worktree 跳过清理: ${wtName} (mode: ${result.mode})`);
1819
+ console.log(` 原因: in-place 模式没有隔离目录需要清理`);
1820
+ } else if (result.result === 'blocked') {
1821
+ console.error(`🚫 拒绝清理:有未落主仓交付变更,请先 sillyspec worktree apply ${wtName} 或 --force`);
1822
+ } else if (result.result === 'partial') {
1823
+ // 坑 ghost-dir-junction-pierce:partial 残留此前落进 else 打成「未找到」,用户误以为
1824
+ // 干净、人工 rm -rf 穿透 junction——必须明示残留 + 安全清理指引
1825
+ console.warn(`⚠️ 部分清理(残留:${(result.residual || []).join('; ') || 'worktree 目录'})`);
1826
+ console.warn(` Windows 手动清理勿 rm -rf(穿透 node_modules junction 删主仓依赖):先 cmd /c rmdir "<worktree>\\node_modules" 解链再删,或 sillyspec worktree doctor --fix`);
1827
+ } else {
1828
+ console.log(`ℹ️ worktree 未找到: ${wtName}`);
1829
+ }
1830
+ } catch (e) {
1831
+ console.error(`❌ ${e.message}`);
1832
+ process.exit(1);
1833
+ }
1834
+ break;
1835
+ }
1836
+ case 'doctor': {
1837
+ const fixFlag = args.includes('--fix');
1838
+ const staleIdx = args.indexOf('--stale-hours');
1839
+ const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
1840
+ const changeIdx = args.indexOf('--change');
1841
+ const changeName = changeIdx !== -1 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
1842
+ const diag = await wm.doctor({ fix: fixFlag, staleHours, changeName });
1843
+ if (diag.issues.length === 0) {
1844
+ console.log('✅ worktree 健康检查通过,无异常');
1845
+ } else {
1846
+ console.log(`🔍 发现 ${diag.issues.length} 个问题:\n`);
1847
+ for (const issue of diag.issues) {
1848
+ const icon = issue.fixable ? '⚠️' : '❌';
1849
+ console.log(` ${icon} [${issue.type}] ${issue.name}: ${issue.detail}`);
1850
+ }
1851
+ if (fixFlag) {
1852
+ console.log(`\n🔧 修复完成:`);
1853
+ for (const f of diag.fixed) console.log(` ✅ ${f}`);
1854
+ if (diag.unfixable.length > 0) {
1855
+ for (const u of diag.unfixable) console.log(` ❌ ${u}`);
1856
+ }
1857
+ if (diag.fixed.length === 0 && diag.unfixable.length === 0) {
1858
+ console.log(' 无需修复');
1859
+ }
1860
+ } else {
1861
+ console.log(`\n💡 运行 sillyspec worktree doctor --fix 自动修复`);
1862
+ }
1863
+ }
1864
+ break;
1865
+ }
1866
+ default:
1867
+ console.error(`❌ 未知子命令: worktree ${wtSubCmd}`);
1868
+ console.log(' 运行 sillyspec worktree --help 查看帮助');
1869
+ process.exit(1);
1870
+ }
1871
+ break;
1872
+ }
1873
+ case 'agent-log': {
1874
+ // 本地 agent 会话日志路径查询/探测(平台会话解析入口):读 <runtimeRoot>/agent-session-log.json
1875
+ //(run 命令在 agent 环境内自动登记),--detect 现场探测不落盘。协议见 docs/platform-agent-log-protocol.md。
1876
+ const { cmdAgentLog } = await import('./agent-session-log.js');
1877
+ await cmdAgentLog(filteredArgs.slice(1), { json, cwd: dir, specDir });
1878
+ break;
1879
+ }
1880
+ case 'dispatch': {
1881
+ // SillyHub 派发抽象层的 agent 调用桥(design.md §Phase2 / D-007@v1):
1882
+ // 仅做能力探测(probe)与派发策略生成(hint),**不执行任何 tool 调用**——
1883
+ // 实际 tool 调用(本机 Agent tool / SillyHub MCP tool)由 agent 据指令执行。
1884
+ // 仿 worktree 子命令的参数解析(filteredArgs[1] 取子命令)与错误处理模式。
1885
+ const dispatchSubCmd = filteredArgs[1];
1886
+ const dispatchSubs = ['probe', 'hint'];
1887
+
1888
+ if (!dispatchSubCmd || dispatchSubCmd === 'help' || dispatchSubCmd === '--help' || dispatchSubCmd === '-h') {
1889
+ console.log(`
1890
+ SillySpec dispatch — SillyHub 派发能力探测与策略生成(agent 调用桥,仅渲染与探测)
1891
+
1892
+ 用法:
1893
+ sillyspec dispatch probe [--json] 能力探测,输出 ProbeResult
1894
+ sillyspec dispatch hint --contract <json> [--json] 派发策略,输出 {instruction, backend}
1895
+
1896
+ 子命令:
1897
+ probe 探测 SillyHub MCP 是否可用(无配置返回 available=false,正常降级不报错)
1898
+ hint 据 contract + probe 结果生成派发指令文本(agent 拿指令后执行实际 tool 调用)
1899
+
1900
+ 选项:
1901
+ --contract <json> hint 必填:DispatchContract(brief/worktreePath/branch/allowedPaths/readOnly/...)
1902
+ --json 程序化输出(probe → ProbeResult;hint → {backend, instruction})
1903
+ `);
1904
+ break;
1905
+ }
1906
+
1907
+ if (dispatchSubCmd === 'probe') {
1908
+ const { probeSillyHub } = await import('./dispatch/probe.js');
1909
+ const result = await probeSillyHub();
1910
+ // result = { available, reason? };无配置时 {available:false,reason:'no-config'} 不报错(正常降级)
1911
+ if (json) {
1912
+ process.stdout.write(JSON.stringify(result));
1913
+ } else if (result.available) {
1914
+ console.log('✅ SillyHub 可用');
1915
+ } else {
1916
+ console.log(`⚠️ SillyHub 不可用:${result.reason || 'unknown'}`);
1917
+ }
1918
+ break;
1919
+ }
1920
+
1921
+ if (dispatchSubCmd === 'hint') {
1922
+ // 解析 --contract <json>(仿 worktree 用 args.indexOf 取 flag 后一个值)
1923
+ const contractIdx = args.indexOf('--contract');
1924
+ const contractRaw = contractIdx >= 0 && args[contractIdx + 1] ? args[contractIdx + 1] : null;
1925
+ if (!contractRaw) {
1926
+ console.error('用法: sillyspec dispatch hint --contract <json> [--json]\n 生成派发指令(DispatchContract:brief/worktreePath/branch/allowedPaths/readOnly/...)');
1927
+ process.exit(2);
1928
+ }
1929
+ let contract;
1930
+ try {
1931
+ contract = JSON.parse(contractRaw);
1932
+ } catch (e) {
1933
+ console.error(`❌ --contract JSON 解析失败: ${e.message}`);
1934
+ process.exit(2);
1935
+ }
1936
+ const { probeSillyHub } = await import('./dispatch/probe.js');
1937
+ const { renderDispatchInstruction } = await import('./dispatch/strategy.js');
1938
+ const probe = await probeSillyHub();
1939
+ const { instruction, backend } = renderDispatchInstruction(contract, probe);
1940
+ if (json) {
1941
+ process.stdout.write(JSON.stringify({ backend, instruction }));
1942
+ } else {
1943
+ console.log(`Backend: ${backend}`);
1944
+ console.log(instruction);
1945
+ }
1946
+ break;
1947
+ }
1948
+
1949
+ // 未知子命令 → did-you-mean 建议 + 退出 1(仿 worktree default)
1950
+ const sug = didYouMean(dispatchSubCmd, dispatchSubs);
1951
+ console.error(`❌ 未知子命令: dispatch ${dispatchSubCmd}`);
1952
+ if (sug) console.error(` 你是想输入「dispatch ${sug}」吗?`);
1953
+ console.error(` 可用子命令:${dispatchSubs.join(' | ')}(运行 sillyspec dispatch 查看用法)`);
1954
+ process.exit(1);
1955
+ break;
1956
+ }
1957
+ case 'platform': {
1958
+ const platformSub = filteredArgs[1];
1959
+ const platformArgs = filteredArgs.slice(2);
1960
+
1961
+ if (!platformSub || platformSub === 'help' || platformSub === '--help' || platformSub === '-h') {
1962
+ console.log(`
1963
+ SillySpec platform — SillyHub 平台同步
1964
+
1965
+ 用法:
1966
+ sillyspec platform connect <url> [--token <token>]
1967
+ sillyspec platform disconnect
1968
+ sillyspec platform sync [--change <name>]
1969
+ sillyspec platform sync-docs [--change <name>]
1970
+ sillyspec platform pull [--change <name>]
1971
+ sillyspec platform resolve <change-name> <--keep-local|--take-platform|--abort>
1972
+ sillyspec platform status
1973
+ sillyspec platform pointer [--cleanup]
1974
+ sillyspec platform approve <change-name>
1975
+ sillyspec platform reject <change-name> [--reason <reason>]
1976
+ `);
1977
+ break;
1978
+ }
1979
+
1980
+ let syncModule;
1981
+ try {
1982
+ syncModule = await import('./sync.js');
1983
+ } catch {
1984
+ console.error('❌ 平台同步功能不可用(sync.js 未实现)');
1985
+ process.exit(1);
1986
+ }
1987
+
1988
+ // 体检 SEC-05:platform 子命令族统一校验 --change 值——此前只有 run 命令族走
1989
+ // assertSafeChangeName,platform sync/pull/resolve 直接透传,冲突文件名
1990
+ // join(runtimeDir, `sync-conflict-${changeName}.json`) 可被 ../ 穿越出 .runtime
1991
+ {
1992
+ const pChangeIdx = args.indexOf('--change');
1993
+ const pChangeVal = pChangeIdx >= 0 && args[pChangeIdx + 1] ? args[pChangeIdx + 1] : null;
1994
+ if (pChangeVal) assertSafeChangeName(pChangeVal, 'platform --change 变更名');
1995
+ }
1996
+
1997
+ switch (platformSub) {
1998
+ case 'pointer': {
1999
+ // 指针状态检查(不依赖 sync 模块)
2000
+ const { readFileSync, existsSync } = await import('fs')
2001
+ const { join } = await import('path')
2002
+ const { POINTER_STATUS, isPointerStale, isPointerCorrupted } = await import('./constants.js')
2003
+ const pointerPath = join(dir, '.sillyspec-platform.json')
2004
+
2005
+ if (!existsSync(pointerPath)) {
2006
+ console.log('ℹ️ 无平台指针文件。当前不在平台模式或未进行过平台 scan。')
2007
+ break
2008
+ }
2009
+
2010
+ try {
2011
+ const pointer = JSON.parse(readFileSync(pointerPath, 'utf8'))
2012
+ console.log(`📄 指针文件: ${pointerPath}`)
2013
+ console.log(` specRoot: ${pointer.specRoot || '(缺失 ❌)'}`)
2014
+ console.log(` runtimeRoot: ${pointer.runtimeRoot || '(未设置)'}`)
2015
+ console.log(` workspaceId: ${pointer.workspaceId || '(未设置)'}`)
2016
+ console.log(` scanRunId: ${pointer.scanRunId || '(未设置)'}`)
2017
+ console.log(` savedAt: ${pointer.savedAt || '(未知)'}`)
2018
+
2019
+ if (isPointerCorrupted(pointer)) {
2020
+ console.log(` 状态: ${POINTER_STATUS.CORRUPTED} ❌`)
2021
+ console.log(` ⚠️ 指针损坏(缺少 specRoot),建议删除后重新运行平台 scan。`)
2022
+ if (platformArgs.includes('--cleanup')) {
2023
+ const { unlinkSync } = await import('fs')
2024
+ unlinkSync(pointerPath)
2025
+ console.log(` 🗑️ 已清理损坏指针。`)
2026
+ console.log(` ℹ️ 如需彻底脱离平台(含接管声明 .sillyspec-platform-managed),请使用 sillyspec platform disconnect。`)
2027
+ }
2028
+ } else if (pointer.status === POINTER_STATUS.SCAN_COMPLETED) {
2029
+ if (isPointerStale(pointer)) {
2030
+ console.log(` 状态: ${POINTER_STATUS.STALE} ⚠️`)
2031
+ console.log(` completedAt: ${pointer.completedAt}`)
2032
+ console.log(` scanStatus: ${pointer.scanStatus || '(未知)'}`)
2033
+ console.log(` ⚠️ 指针已过时(完成超过 24h),可以安全删除。`)
2034
+ if (platformArgs.includes('--cleanup')) {
2035
+ const { unlinkSync } = await import('fs')
2036
+ unlinkSync(pointerPath)
2037
+ console.log(` 🗑️ 已清理过时指针。`)
2038
+ console.log(` ℹ️ 如需彻底脱离平台(含接管声明 .sillyspec-platform-managed),请使用 sillyspec platform disconnect。`)
2039
+ }
2040
+ } else {
2041
+ console.log(` 状态: ${pointer.status} ✅`)
2042
+ console.log(` completedAt: ${pointer.completedAt}`)
2043
+ console.log(` scanStatus: ${pointer.scanStatus || '(未知)'}`)
2044
+ }
2045
+ } else {
2046
+ console.log(` 状态: ${POINTER_STATUS.ACTIVE} 🔄`)
2047
+ }
2048
+ } catch (e) {
2049
+ console.log(` 状态: ${POINTER_STATUS.CORRUPTED} ❌`)
2050
+ console.log(` ⚠️ 指针文件损坏: ${e.message}`)
2051
+ }
2052
+ break;
2053
+ }
2054
+ case 'connect': {
2055
+ const url = platformArgs[0];
2056
+ if (!url) {
2057
+ console.error('❌ 用法: sillyspec platform connect <url> [--token <token>]');
2058
+ process.exit(1);
2059
+ }
2060
+ const tokenIdx = args.indexOf('--token');
2061
+ const token = tokenIdx >= 0 && args[tokenIdx + 1] ? args[tokenIdx + 1] : undefined;
2062
+ if (!token) {
2063
+ // 缺 token 直接终止(体检 HUB-02):交互式输入尚未实现(task-11),此前
2064
+ // 继续执行会把 undefined 序列化成字符串 "undefined" 写入 local.yaml,
2065
+ // 报"连接成功"后所有同步持续 401(Bearer undefined)
2066
+ console.error('❌ 缺少 --token <token>(交互式输入暂未实现);请在 SillyHub 平台获取 token 后重试');
2067
+ process.exit(1);
2068
+ }
2069
+ await syncModule.connect(url, token, dir);
2070
+ break;
2071
+ }
2072
+ case 'disconnect':
2073
+ await syncModule.disconnect(dir);
2074
+ break;
2075
+ case 'sync': {
2076
+ const syncChangeIdx = args.indexOf('--change');
2077
+ const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] ? args[syncChangeIdx + 1] : null;
2078
+ await syncModule.sync(syncChangeName, dir);
2079
+ break;
2080
+ }
2081
+ case 'sync-docs': {
2082
+ const syncDocsChangeIdx = args.indexOf('--change');
2083
+ const syncDocsChangeName = syncDocsChangeIdx >= 0 && args[syncDocsChangeIdx + 1] ? args[syncDocsChangeIdx + 1] : null;
2084
+ await syncModule.syncDocuments(syncDocsChangeName, dir);
2085
+ break;
2086
+ }
2087
+ case 'status': {
2088
+ // task-14 / FR-05:扩展 status(落后标记 + 未决冲突列表);未连接输出与现状一致
2089
+ const st = await syncModule.collectStatus(dir);
2090
+ if (!st.connected) {
2091
+ console.log('平台: 未连接');
2092
+ break;
2093
+ }
2094
+ console.log(`平台: ${st.url}`);
2095
+ console.log(`上次连接: ${st.lastSync || '未知'}`);
2096
+ if (st.listFailed) {
2097
+ console.log('ℹ️ 无法连接平台获取落后状态');
2098
+ } else if (st.behind.length > 0) {
2099
+ console.log(`⚠️ 本地可能落后(${st.behind.length} 个变更平台有更新):`);
2100
+ for (const b of st.behind) {
2101
+ console.log(` - ${b.name}: 平台 ${b.platform_pushed_at} > 本地 ${b.local_synced}`);
2102
+ }
2103
+ } else {
2104
+ console.log('✅ 本地与平台进度同步');
2105
+ }
2106
+ if (st.conflicts.length > 0) {
2107
+ console.log(`⚠️ 未决冲突(${st.conflicts.length} 个,请 platform resolve 处理):`);
2108
+ for (const c of st.conflicts) {
2109
+ console.log(` - ${c.change}${c.created_at ? ` (创建于 ${c.created_at})` : ''}`);
2110
+ }
2111
+ }
2112
+ break;
2113
+ }
2114
+ case 'pull': {
2115
+ // task-11 / D-006 / D-009 / FR-03:手动下行拉取平台进度到本地。
2116
+ // 与自动 triggerPull 共用 syncModule.pull(SyncManager.pull 实例方法),行为一致。
2117
+ const pullChangeIdx = args.indexOf('--change');
2118
+ const pullChangeName = pullChangeIdx >= 0 && args[pullChangeIdx + 1] ? args[pullChangeIdx + 1] : null;
2119
+ // 未连接明确提示不崩(acceptance:未连接平台输出明确提示)
2120
+ const smForCheck = new syncModule.SyncManager(dir);
2121
+ if (!smForCheck._getPlatform()) {
2122
+ console.error('❌ 未连接平台,请先 sillyspec platform connect');
2123
+ process.exit(1);
2124
+ }
2125
+ if (pullChangeName) {
2126
+ // 单变更完整 pull(两级 pull 第二级)
2127
+ const r = await syncModule.pull(pullChangeName, {}, dir);
2128
+ if (r.conflict) {
2129
+ console.log(`⚠️ ${pullChangeName}: 冲突(已写 sync-conflict 文件),请 sillyspec platform resolve 处理`);
2130
+ } else if (r.imported) {
2131
+ console.log(`✅ ${pullChangeName}: 已拉取并 import`);
2132
+ } else {
2133
+ console.log(`⏭️ ${pullChangeName}: 未 import(${r.reason || '无变更'})`);
2134
+ }
2135
+ } else {
2136
+ // 无 --change:先拉轻量列表(两级 pull 第一级),再对每个 change 按需完整 pull
2137
+ const list = await syncModule.pullList(dir);
2138
+ if (!list.ok) {
2139
+ console.error(`❌ 拉取变更列表失败: ${list.reason || '未知'}`);
2140
+ process.exit(1);
2141
+ }
2142
+ console.log(`平台变更 ${list.changes.length} 个:`);
2143
+ let importedCount = 0, conflictCount = 0;
2144
+ for (const ch of list.changes) {
2145
+ const name = typeof ch === 'string' ? ch : ch.name;
2146
+ if (!name) continue;
2147
+ const r = await syncModule.pull(name, {}, dir);
2148
+ if (r.imported) { importedCount++; console.log(` ✅ ${name}: 已 import`); }
2149
+ else if (r.conflict) { conflictCount++; console.log(` ⚠️ ${name}: 冲突,请 platform resolve`); }
2150
+ else { console.log(` ⏭️ ${name}: ${r.reason || '未 import'}`); }
2151
+ }
2152
+ console.log(`拉取完成:imported ${importedCount},冲突 ${conflictCount}`);
2153
+ }
2154
+ break;
2155
+ }
2156
+ case 'resolve': {
2157
+ // task-13 / D-002 / D-010 / D-013 / FR-05:冲突解决三选一(绝不字段级 auto-merge)
2158
+ // 变更名解析(顺序):--change <name>(与 pull/sync 兄弟命令写法一致)→ platformArgs 中
2159
+ // 第一个非 flag 位置参数 → 唯一未决冲突自动选中。修复:旧实现盲取 platformArgs[0],
2160
+ // flag 放前面时(resolve --keep-local --change x)把 '--keep-local' 当变更名去读冲突文件。
2161
+ const changeIdx = args.indexOf('--change');
2162
+ let resolveName = changeIdx >= 0 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
2163
+ if (!resolveName) {
2164
+ resolveName = platformArgs.find((a) => a && !a.startsWith('--')) || null;
2165
+ }
2166
+ if (!resolveName) {
2167
+ // 无变更名:恰有一个未决冲突时自动选中,多个/零个则列出候选
2168
+ const pendingConflicts = syncModule.listConflictFiles(dir);
2169
+ if (pendingConflicts.length === 1) {
2170
+ resolveName = pendingConflicts[0].change;
2171
+ console.log(`ℹ️ 自动选中唯一未决冲突: ${resolveName}`);
2172
+ } else {
2173
+ console.error('❌ 用法: sillyspec platform resolve <change-name> <--keep-local|--take-platform|--abort>');
2174
+ console.error(' 或: sillyspec platform resolve --change <name> <--keep-local|--take-platform|--abort>');
2175
+ if (pendingConflicts.length > 1) {
2176
+ console.error(` 现有 ${pendingConflicts.length} 个未决冲突,请指定变更名:`);
2177
+ for (const c of pendingConflicts) console.error(` - ${c.change}`);
2178
+ } else {
2179
+ console.error(' 当前无 sync-conflict 文件(可先运行 sillyspec platform status 查看)');
2180
+ }
2181
+ process.exit(1);
2182
+ }
2183
+ }
2184
+ // 解析 mode flag(三选一互斥,多传/不传均报错)
2185
+ const resolveFlags = ['--keep-local', '--take-platform', '--abort'].filter((f) => args.includes(f));
2186
+ if (resolveFlags.length !== 1) {
2187
+ console.error('❌ 必须恰好指定 --keep-local / --take-platform / --abort 之一');
2188
+ process.exit(1);
2189
+ }
2190
+ // 位置参数变更名与 --change 同规格校验(SEC-05:resolve 会按该名重推/删冲突文件)
2191
+ assertSafeChangeName(resolveName, 'platform resolve 变更名');
2192
+ const modeMap = { '--keep-local': 'keep-local', '--take-platform': 'take-platform', '--abort': 'abort' };
2193
+ const resolveMode = modeMap[resolveFlags[0]];
2194
+ const r = await syncModule.resolve(resolveName, resolveMode, dir);
2195
+ if (r.ok && r.resolved) {
2196
+ console.log(`✅ ${resolveName} [${resolveMode}]: ${r.reason}`);
2197
+ } else {
2198
+ console.error(`❌ ${resolveName}: ${r.reason}`);
2199
+ // 报错兜底:指定变更名无冲突文件时,列出 .runtime 下实际存在的冲突文件(防 flag 误当变更名再次误导)
2200
+ const pendingConflicts = syncModule.listConflictFiles(dir);
2201
+ if (pendingConflicts.length > 0 && !pendingConflicts.some((c) => c.change === resolveName)) {
2202
+ console.error(' 当前未决冲突:');
2203
+ for (const c of pendingConflicts) console.error(` - ${c.change}`);
2204
+ }
2205
+ process.exit(1);
2206
+ }
2207
+ break;
2208
+ }
2209
+ case 'approve': {
2210
+ const approveName = platformArgs[0];
2211
+ if (!approveName) {
2212
+ console.error('❌ 用法: sillyspec platform approve <change-name>');
2213
+ process.exit(1);
2214
+ }
2215
+ // 审批前下行 pull(task-10 / D-009 / FR-06):拉最新进度避免基于过期状态审批;未连接跳过
2216
+ const { triggerPull } = await import('./run/shared.js');
2217
+ await triggerPull(dir, approveName);
2218
+ await syncModule.approve(approveName, dir);
2219
+ break;
2220
+ }
2221
+ case 'reject': {
2222
+ const rejectName = platformArgs[0];
2223
+ if (!rejectName) {
2224
+ console.error('❌ 用法: sillyspec platform reject <change-name> [--reason <reason>]');
2225
+ process.exit(1);
2226
+ }
2227
+ const reasonIdx = args.indexOf('--reason');
2228
+ const reason = reasonIdx >= 0 && args[reasonIdx + 1] ? args[reasonIdx + 1] : undefined;
2229
+ await syncModule.reject(rejectName, reason, dir);
2230
+ break;
2231
+ }
2232
+ default:
2233
+ console.error(`❌ 未知子命令: platform ${platformSub}`);
2234
+ console.log(' 运行 sillyspec platform --help 查看帮助');
2235
+ process.exit(1);
2236
+ }
2237
+ break;
2238
+ }
2239
+ case 'taskcard': {
2240
+ const tcName = filteredArgs.slice(1).find(a => !a.startsWith('-'));
2241
+ if (!tcName || tcName === 'help' || tcName === '--help' || tcName === '-h') {
2242
+ console.log(`
2243
+ SillySpec taskcard — 生成 Windows 安全的 TaskCard 骨架
2244
+
2245
+ 用法:
2246
+ sillyspec taskcard <change-name> --task task-01[,task-02...] [--title <t>] [--title-zh <t>] [--set key=value]... [--force]
2247
+ sillyspec taskcard <change-name> --all [--set key=value]... [--force]
2248
+
2249
+ 骨架由 CLI 直写(LF 行尾 + frontmatter 闭合 + 硬校验 9 字段齐全),标题自动取自 tasks.md
2250
+ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";--set 批量注入 design
2251
+ 提炼的字段值(priority 等,yamlScalar 转义)。之后用 Edit 填充占位符即可,勿手写整卡。
2252
+ `);
2253
+ break;
2254
+ }
2255
+ try { assertSafeChangeName(tcName, '变更名'); }
2256
+ catch (e) { console.error(`❌ ${e.message}`); process.exit(2); }
2257
+
2258
+ const force = filteredArgs.includes('--force');
2259
+ const all = filteredArgs.includes('--all');
2260
+ const getVal = (flag) => {
2261
+ const idx = filteredArgs.indexOf(flag);
2262
+ return idx !== -1 && filteredArgs[idx + 1] && !filteredArgs[idx + 1].startsWith('-')
2263
+ ? filteredArgs[idx + 1] : null;
2264
+ };
2265
+ const taskVal = getVal('--task');
2266
+ const titleVal = getVal('--title');
2267
+ const titleZhVal = getVal('--title-zh');
2268
+ // --set key=value(可重复,坑 taskcard-design-field-conflicts):批量生成时从 design/
2269
+ // plan 提炼的字段值直接注入骨架(yamlScalar 转义、白名单键校验在 buildTaskcardSkeleton),
2270
+ // 省主代理逐卡 Edit 裁决
2271
+ const sets = {};
2272
+ for (let i = 0; i < filteredArgs.length; i++) {
2273
+ if (filteredArgs[i] === '--set' && filteredArgs[i + 1]) {
2274
+ const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(filteredArgs[i + 1]);
2275
+ if (!m) {
2276
+ console.error(`❌ --set 格式非法:${filteredArgs[i + 1]}(应为 key=value,可重复)`);
2277
+ process.exit(2);
2278
+ }
2279
+ sets[m[1]] = m[2];
2280
+ }
2281
+ }
2282
+
2283
+ if (all && taskVal) {
2284
+ console.error('❌ --all 与 --task 互斥(--all 取 plan.md 全部任务,--task 显式指定)');
2285
+ process.exit(1);
2286
+ }
2287
+ if (!all && !taskVal) {
2288
+ console.error('❌ 用法: sillyspec taskcard <change-name> --task task-01[,task-02...] | --all [--force]');
2289
+ process.exit(1);
2290
+ }
2291
+
2292
+ let taskIds;
2293
+ if (all) {
2294
+ taskIds = 'all';
2295
+ } else {
2296
+ const { normalizeTaskId } = await import('./taskcard.js');
2297
+ try {
2298
+ taskIds = taskVal.split(',').map(s => s.trim()).filter(Boolean).map(normalizeTaskId);
2299
+ } catch (e) {
2300
+ console.error(`❌ ${e.message}`);
2301
+ process.exit(1);
2302
+ }
2303
+ }
2304
+
2305
+ const { cmdTaskcard } = await import('./taskcard.js');
2306
+ try {
2307
+ const result = cmdTaskcard(tcName, { cwd: dir, specDir, taskIds, title: titleVal, titleZh: titleZhVal, force, sets });
2308
+ for (const f of result.created) console.log(`✅ 已生成: ${f}`);
2309
+ for (const f of result.skipped) console.log(`⏭️ 已存在,跳过(--force 覆盖): ${f}`);
2310
+ if (result.created.length > 0) {
2311
+ console.log(` 下一步:用 Edit tool 填充占位符(allowed_paths/goal/implementation/acceptance/verify/constraints),勿用 Write 整文件重写。`);
2312
+ }
2313
+ } catch (e) {
2314
+ console.error(`❌ ${e.message}`);
2315
+ process.exit(1);
2316
+ }
2317
+ break;
2318
+ }
2319
+ case 'change-rename': {
2320
+ const oldName = filteredArgs[1];
2321
+ const newName = filteredArgs[2];
2322
+ if (!oldName || !newName) {
2323
+ console.error('❌ 用法: sillyspec change-rename <旧变更名> <新变更名>');
2324
+ process.exit(1);
2325
+ }
2326
+ // F6 路径穿越消毒:renameChange 会 mv changes/<old> → changes/<new>,名含 ../ 会逃出 changes/。
2327
+ try {
2328
+ assertSafeChangeName(oldName, '旧变更名');
2329
+ assertSafeChangeName(newName, '新变更名');
2330
+ } catch (e) {
2331
+ console.error(`❌ ${e.message}`);
2332
+ process.exit(2);
2333
+ }
2334
+ const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
2335
+ pm.renameChange(dir, oldName, newName);
2336
+ break;
2337
+ }
2338
+ case 'workflow': {
2339
+ const wfSub = filteredArgs[1];
2340
+ if (!wfSub || wfSub === 'help' || wfSub === '--help') {
2341
+ console.log(`
2342
+ SillySpec workflow — 工作流管理
2343
+
2344
+ 用法:
2345
+ sillyspec workflow check <name> [--project <project>] [--json]
2346
+ sillyspec workflow list
2347
+ `);
2348
+ break;
2349
+ }
2350
+ if (wfSub === 'list') {
2351
+ const { listWorkflows } = await import('./workflow.js');
2352
+ const names = listWorkflows(dir);
2353
+ if (names.length === 0) {
2354
+ console.log('未找到 workflow 定义(.sillyspec/workflows/*.yaml)');
2355
+ } else {
2356
+ console.log(`可用 workflow:`);
2357
+ for (const name of names) {
2358
+ const { loadWorkflow } = await import('./workflow.js');
2359
+ const wf = loadWorkflow(dir, name);
2360
+ const specVer = wf?.spec_version || wf?.version || '?';
2361
+ const mode = wf?.orchestration?.mode || '?';
2362
+ const roles = wf?.roles?.length || 0;
2363
+ console.log(` ${name} (spec v${specVer}, ${mode}, ${roles} roles)`);
2364
+ }
2365
+ }
2366
+ break;
2367
+ }
2368
+ if (wfSub === 'check') {
2369
+ const { loadWorkflow, runPostCheck, listWorkflows, saveWorkflowRun } = await import('./workflow.js');
2370
+ const wfName = filteredArgs[2];
2371
+ if (!wfName) {
2372
+ console.error('❌ 请指定 workflow 名称,例如:sillyspec workflow check scan-docs --project sillyspec');
2373
+ process.exit(2);
2374
+ }
2375
+ const wf = loadWorkflow(dir, wfName);
2376
+ if (!wf) {
2377
+ console.error(`❌ 未找到 workflow: ${wfName}`);
2378
+ console.error(`可用 workflow:${listWorkflows(dir).join(', ') || '无'}`);
2379
+ process.exit(2);
2380
+ }
2381
+ // depends_on 校验
2382
+ if (wf._validationErrors && wf._validationErrors.length > 0) {
2383
+ console.error('❌ workflow YAML 校验失败:');
2384
+ for (const err of wf._validationErrors) {
2385
+ console.error(` ${err}`);
2386
+ }
2387
+ process.exit(2);
2388
+ }
2389
+ // spec_version 校验
2390
+ const specVer = wf.spec_version || wf.version;
2391
+ if (!specVer) {
2392
+ console.error('❌ workflow YAML 缺少 spec_version 字段');
2393
+ process.exit(2);
2394
+ }
2395
+ const SUPPORTED_SPECS = [1];
2396
+ if (!SUPPORTED_SPECS.includes(specVer)) {
2397
+ console.error(`❌ 不支持的 spec_version: ${specVer}(支持: ${SUPPORTED_SPECS.join(', ')})`);
2398
+ process.exit(2);
2399
+ }
2400
+ // 解析 --project
2401
+ const projectIdx = filteredArgs.indexOf('--project');
2402
+ const project = projectIdx !== -1 && filteredArgs[projectIdx + 1] ? filteredArgs[projectIdx + 1] : null;
2403
+ // 解析 --json(已在顶层解析)
2404
+ const isJson = json;
2405
+ // 解析 --change
2406
+ const changeIdx = filteredArgs.indexOf('--change');
2407
+ const changeName = changeIdx !== -1 && filteredArgs[changeIdx + 1] ? filteredArgs[changeIdx + 1] : null;
2408
+
2409
+ if (!project && wfName !== 'archive-impact') {
2410
+ console.error('❌ 请指定 --project,例如:--project sillyspec');
2411
+ process.exit(2);
2412
+ }
2413
+
2414
+ // 执行检查
2415
+ let resolvedWf = wf;
2416
+ const placeholders = {};
2417
+ if (changeName) placeholders['change-name'] = changeName;
2418
+ // 替换占位符
2419
+ let jsonStr = JSON.stringify(resolvedWf);
2420
+ if (changeName) jsonStr = jsonStr.replace(/<change-name>/g, changeName);
2421
+ resolvedWf = JSON.parse(jsonStr);
2422
+
2423
+ const projectName = project || 'sillyspec';
2424
+ const result = runPostCheck(resolvedWf, dir, projectName, placeholders);
2425
+
2426
+ if (isJson) {
2427
+ console.log(JSON.stringify(result, null, 2));
2428
+ } else {
2429
+ // 带项目维度前缀的输出(从统一结果对象格式化)
2430
+ const lines = [`\n📋 Workflow Post-Check: ${result.workflow} (project: ${result.project})\n`];
2431
+ for (const r of (result.roles || [])) {
2432
+ const icon = r.status === 'pass' ? '✅' : '❌';
2433
+ lines.push(`${icon} [${result.project}] ${r.name} (${r.id})`);
2434
+ const roleFailures = (result.failures || []).filter(f => f.role_id === r.id);
2435
+ for (const f of roleFailures) {
2436
+ lines.push(` └─ ${f.message}`);
2437
+ }
2438
+ }
2439
+ const wfFailures = (result.workflow_checks || []).filter(c => c.status === 'fail');
2440
+ if (wfFailures.length > 0) {
2441
+ lines.push('');
2442
+ for (const f of wfFailures) {
2443
+ lines.push(`❌ [${result.project}] 全局: ${f.detail}`);
2444
+ }
2445
+ }
2446
+ lines.push('');
2447
+ if (result.status === 'pass') {
2448
+ lines.push('✅ 全部检查通过');
2449
+ } else {
2450
+ lines.push('❌ 存在失败项');
2451
+ }
2452
+ console.log(lines.join('\n'));
2453
+ }
2454
+
2455
+ // exit code: 0=通过, 1=检查失败, 2=参数/YAML错误
2456
+ if (saveWorkflowRunFlag) {
2457
+ const saved = saveWorkflowRun(result, { cwd: dir, source: 'cli' });
2458
+ if (saved) {
2459
+ if (!isJson) console.log(`\n📁 结果已归档:${saved}`);
2460
+ }
2461
+ }
2462
+ process.exit(result.status === 'pass' ? 0 : 1);
2463
+ } else {
2464
+ const wfSubs = ['check', 'list'];
2465
+ const sug = didYouMean(wfSub, wfSubs);
2466
+ console.error(`❌ 未知子命令: workflow ${wfSub}`);
2467
+ if (sug) console.error(` 你是想输入「workflow ${sug}」吗?`);
2468
+ console.error(` 可用子命令:${wfSubs.join(' | ')}(运行 sillyspec workflow 查看用法)`);
2469
+ process.exit(1);
2470
+ }
2471
+ break;
2472
+ }
2473
+ case 'modules': {
2474
+ const modulesSub = filteredArgs[1];
2475
+ if (!modulesSub || modulesSub === 'help' || modulesSub === '--help') {
2476
+ console.log(`
2477
+ SillySpec modules — 模块文档管理
2478
+
2479
+ 用法:
2480
+ sillyspec modules rebuild [--force] 从模块卡片 + 源码重建 _module-map.yaml(默认 dry-run 预览,--force 才覆盖)
2481
+ sillyspec modules status 显示模块索引状态
2482
+ sillyspec modules migrate 旧格式模块文档迁移到新格式
2483
+ sillyspec modules resolve --change <名> [--spec-dir <path>] [--json]
2484
+ per-task 模块卡级联解析(跨全部 _module-map.yaml 最长前缀匹配,
2485
+ 子项目细卡优先于根层大卡)——execute 加载上下文同源注入
2486
+ sillyspec modules split-changelog [--force]
2487
+ 把模块卡「变更索引」历史段迁出到 <module>.changelog.md sidecar
2488
+ (默认 dry-run 预览,--force 才写入;治大卡单调膨胀)
2489
+ `);
2490
+ break;
2491
+ }
2492
+ if (modulesSub === 'rebuild') {
2493
+ const { rebuildModuleMap } = await import('./modules.js');
2494
+ const isForce = filteredArgs.includes('--force');
2495
+ await rebuildModuleMap(dir, { force: isForce });
2496
+ } else if (modulesSub === 'status') {
2497
+ const { showModuleStatus } = await import('./modules.js');
2498
+ await showModuleStatus(dir);
2499
+ } else if (modulesSub === 'migrate') {
2500
+ const { migrateModuleDocs } = await import('./modules.js');
2501
+ await migrateModuleDocs(dir);
2502
+ } else if (modulesSub === 'split-changelog') {
2503
+ const { splitChangelog } = await import('./modules.js');
2504
+ const scForce = filteredArgs.includes('--force');
2505
+ splitChangelog(dir, { force: scForce });
2506
+ } else if (modulesSub === 'resolve') {
2507
+ // per-task 模块卡级联解析(token 成本优化 P0a):execute「加载上下文」步的
2508
+ // {MODULE_RESOLVE_TABLE} 注入同源;独立命令供 agent/人随时查(细卡优先,防整读大卡)
2509
+ const mrChangeIdx = args.indexOf('--change');
2510
+ const mrChange = mrChangeIdx >= 0 && args[mrChangeIdx + 1] ? args[mrChangeIdx + 1] : null;
2511
+ if (!mrChange) {
2512
+ console.error('用法: sillyspec modules resolve --change <名> [--spec-dir <path>] [--json]\n 输出 per-task 最优模块卡表(跨全部 _module-map.yaml 最长前缀匹配,细卡优先)');
2513
+ process.exit(2);
2514
+ }
2515
+ assertSafeChangeName(mrChange, '--change 变更名');
2516
+ const mrSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
2517
+ const { resolveChangeModuleCards, renderModuleResolveTable } = await import('./module-resolve.js');
2518
+ if (filteredArgs.includes('--json')) {
2519
+ const r = resolveChangeModuleCards({ cwd: dir, specBase: mrSpecBase, changeName: mrChange });
2520
+ console.log(JSON.stringify(r, null, 2));
2521
+ } else {
2522
+ console.log(renderModuleResolveTable({ cwd: dir, specBase: mrSpecBase, changeName: mrChange }));
2523
+ }
2524
+ } else {
2525
+ const modSubs = ['rebuild', 'status', 'migrate', 'resolve', 'split-changelog'];
2526
+ const sug = didYouMean(modulesSub, modSubs);
2527
+ console.error(`❌ 未知子命令: modules ${modulesSub}`);
2528
+ if (sug) console.error(` 你是想输入「modules ${sug}」吗?`);
2529
+ console.error(` 可用子命令:${modSubs.join(' | ')}(运行 sillyspec modules 查看用法)`);
2530
+ process.exit(1);
2531
+ }
2532
+ break;
2533
+ }
2534
+ case 'local': {
2535
+ // 本地配置探测(task-04 / D-001@v1):纯 fs 嗅探项目类型 → 生成 local.yaml。
2536
+ // 轻量独立路由,不跑 scan、不消耗 token。探测逻辑归属 local-detect.js(task-02)。
2537
+ const localSubCmd = filteredArgs[1];
2538
+ // register-repo(2026-08-21 审计项④):跨仓 repo key 注册命令化。此前靠 agent 手编
2539
+ // local.yaml repos: 段(正是 agent 满目录找/根目录乱建 local.yaml 的上游);execute
2540
+ // 启动 fail-closed 报错已列出缺的 key,本命令外科手术式写入——只动 repos: 段,
2541
+ // 注释/凭据段逐行保留。写目标解析 worktree→主仓(config-cat.js 同源候选链)。
2542
+ if (localSubCmd === 'register-repo') {
2543
+ const rrKey = filteredArgs[2];
2544
+ const rrPath = filteredArgs[3];
2545
+ if (!rrKey || !rrPath || rrPath.startsWith('-')) {
2546
+ console.error('用法: sillyspec local register-repo <key> <path>\n 注册跨仓仓到 local.yaml repos: 段(跨仓 task 卡 repo: 的 key 必须先注册,execute 启动 fail-closed 校验)\n key 限字母数字._-;path 为跨仓仓根目录(相对路径按当前 cwd 解析,须是 git 仓);main 隐式不用注册');
2547
+ process.exit(2);
2548
+ }
2549
+ let rrSpecBase = null;
2550
+ try {
2551
+ rrSpecBase = resolvePlatformSpecDir(dir, specDir);
2552
+ } catch (e) {
2553
+ // fail-closed:平台 pointer 异常时不盲写(写错 local.yaml 比不写更糟),报修复引导
2554
+ console.error(`❌ 无法解析 local.yaml 目标路径(平台指针异常): ${e.message}`);
2555
+ process.exit(1);
2556
+ }
2557
+ const absRepo = resolve(rrPath);
2558
+ if (!existsSync(absRepo)) {
2559
+ console.error(`❌ 路径不存在: ${absRepo}`);
2560
+ process.exit(1);
2561
+ }
2562
+ if (!(await import('./git-helper.js')).gitQuiet(absRepo, ['rev-parse', '--git-dir'])) {
2563
+ console.error(`❌ 不是 git 仓库: ${absRepo}(repos: 段注册的跨仓仓必须可 rev-parse,MultiRepoContext 会在其中跑 git)`);
2564
+ process.exit(1);
2565
+ }
2566
+ const { resolveLocalYamlWriteTarget } = await import('./config-cat.js');
2567
+ const target = resolveLocalYamlWriteTarget(dir, { specBase: rrSpecBase });
2568
+ const { registerRepoInLocalYaml } = await import('./local-register.js');
2569
+ let rrResult;
2570
+ try {
2571
+ rrResult = registerRepoInLocalYaml(target.path, rrKey, absRepo);
2572
+ } catch (e) {
2573
+ console.error(`❌ 注册失败: ${e.message}`);
2574
+ process.exit(1);
2575
+ }
2576
+ const verb = rrResult.replaced ? '已更新(覆盖旧路径)' : '已注册';
2577
+ console.log(`✅ repos.${rrKey} ${verb}: ${absRepo.replace(/\\/g, '/')}`);
2578
+ console.log(` 写入: ${target.path}${rrResult.fileCreated ? '(新建文件)' : ''}${rrResult.sectionCreated ? '(新建 repos: 段)' : ''}`);
2579
+ console.log(' 查看: sillyspec config cat(真实配置); main 隐式=主仓不用注册');
2580
+ break;
2581
+ }
2582
+ if (localSubCmd !== 'detect') {
2583
+ console.error('用法: sillyspec local detect [--dir <path>]\n sillyspec local register-repo <key> <path>\n detect: 纯 fs 嗅探项目类型并生成 local.yaml(不跑 scan、零 token)\n register-repo: 注册跨仓仓到 local.yaml repos: 段(外科手术式写入,其余内容保留)');
2584
+ process.exit(2);
2585
+ }
2586
+
2587
+ const detected = (await import('./local-detect.js')).detectLocalYaml(dir);
2588
+ const specRoot = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
2589
+ const localYamlPath = join(specRoot, 'local.yaml');
2590
+
2591
+ if (existsSync(localYamlPath)) {
2592
+ console.log(`ℹ️ local.yaml 已存在,跳过: ${localYamlPath}`);
2593
+ break;
2594
+ }
2595
+
2596
+ // 序列化为 local.yaml 文本(与 scan.js 生成本地配置步骤的 yaml 模板格式一致,向后兼容)
2597
+ const c = detected.commands || {};
2598
+ const lines = [];
2599
+ lines.push('# SillySpec 本地配置(自动生成,可手动修改)');
2600
+ lines.push('project:');
2601
+ lines.push(` type: ${detected.project.type} # nodejs/maven/gradle/make/generic`);
2602
+ lines.push('');
2603
+ lines.push('commands:');
2604
+ if (c.build) lines.push(` build: "${c.build}"`);
2605
+ if (c.test) lines.push(` test: "${c.test}"`);
2606
+ if (c.lint) lines.push(` lint: "${c.lint}"`);
2607
+ lines.push('');
2608
+ lines.push('# 测试策略:full=全量测试, module=只测变更模块, skip=跳过测试');
2609
+ lines.push('test_strategy: module');
2610
+ lines.push('');
2611
+ lines.push('# monorepo 子模块映射(test_strategy: module 时按 git diff 命中模块收窄测试;inline flow 形态)');
2612
+ lines.push('# modules:');
2613
+ lines.push('# user-service: { path: "user/", test: "cd user && npm test" }');
2614
+ lines.push('# order-service: { path: "order/", test: "cd order && npm test" }');
2615
+ lines.push('# 已知预存失败(可选):verify 实测时这些失败被豁免、不阻断归档;fail-safe 仅豁免能检测到的失败行');
2616
+ lines.push('# known_failures:');
2617
+ lines.push('# - "tests/test_legacy.py::test_old"');
2618
+ const yamlText = lines.join('\n') + '\n';
2619
+
2620
+ // 原子写(tmp + rename + Windows EPERM 重试),避免半截 local.yaml
2621
+ mkdirSync(specRoot, { recursive: true });
2622
+ writeAtomicSync(localYamlPath, yamlText);
2623
+
2624
+ console.log(`✅ 已生成 local.yaml (type: ${detected.project.type})`);
2625
+ console.log(` 路径: ${localYamlPath}`);
2626
+ break;
2627
+ }
2628
+ case 'config': {
2629
+ // local.yaml 键发现缺口(2026-08-11):键散落 ~10 个 reader,外部项目 agent 无从得知。
2630
+ // config schema 把全部已知键 + 生效状态 + 读取点打印出来,堵住发现缺口。
2631
+ // 数据源唯一 = src/config-schema.js(与 init 落盘的 local.yaml.example 同源)。
2632
+ const configSub = filteredArgs[1];
2633
+ const wantHelp = configSub === 'help' || configSub === '--help' || configSub === '-h';
2634
+ const isSchema = !configSub || configSub === 'schema';
2635
+ if (wantHelp) {
2636
+ console.log(`
2637
+ SillySpec config — local.yaml 配置键速查
2638
+
2639
+ 用法:
2640
+ sillyspec config 打印 local.yaml 全部已知键(人类可读树,默认)
2641
+ sillyspec config schema 同上(显式子命令)
2642
+ sillyspec config --json 机读 JSON(程序化消费)
2643
+ sillyspec config schema --json 同上
2644
+ sillyspec config cat 读取 local.yaml 实际内容(候选链:spec 根 → cwd 祖先链 →
2645
+ git common-dir 主仓根;worktree 内 .sillyspec 是 checkout
2646
+ 副本无此文件,cat 自动解析到主仓)
2647
+ sillyspec config cat --json 机读 JSON(dir/path/source/content;未找到退出 1)
2648
+
2649
+ 说明:
2650
+ - 数据源:src/config-schema.js(唯一真相;reader 见各键「读取点」)
2651
+ - 键分两类:【生效】配了即生效;【声明但未接线】代码/JSDoc 提及但无 reader,配了不生效
2652
+ - 脱敏示例文件:sillyspec init 生成 local.yaml.example(可提交;真实 local.yaml 是 gitignored)
2653
+ - local.yaml 恒在 .sillyspec/ 下——项目根目录没有这个文件,别去那里找(写 .sillyspec/
2654
+ 之外的同名文件会被 hook 拦截)
2655
+ `);
2656
+ break;
2657
+ }
2658
+ // config cat(2026-08-21 root-local-yaml 治理):schema 只列键不含值,agent 拿实际配置
2659
+ // 只能翻文件——worktree 内 .sillyspec 是 checkout 副本(gitignored 的 local.yaml 不随
2660
+ // checkout 出现),找不到就漂去项目根乱找/乱建。cat = 唯一权威读取口,按「文件存在」
2661
+ // 解析真实路径(config-cat.js:spec 根 → cwd 祖先链 → git common-dir 主仓根)。
2662
+ // agent 本就有 shell 权限可自行 cat,CLI 回显明文不增加暴露面。
2663
+ if (configSub === 'cat') {
2664
+ let specBase = null
2665
+ try { specBase = resolvePlatformSpecDir(dir, specDir) } catch { /* 平台 pointer 异常 → 走祖先链(真实 reader 各自 fail-closed,cat 只读不加重) */ }
2666
+ const { resolveLocalYaml } = await import('./config-cat.js')
2667
+ const r = resolveLocalYaml(dir, { specBase })
2668
+ if (json) {
2669
+ const payload = { cmd: 'config cat', dir: resolve(dir), exists: r.exists, path: r.path, source: r.source, searched: r.searched }
2670
+ if (r.exists) payload.content = readFileSync(r.path, 'utf8')
2671
+ console.log(JSON.stringify(payload, null, 2))
2672
+ if (!r.exists) process.exitCode = 1
2673
+ break
2674
+ }
2675
+ if (!r.exists) {
2676
+ console.error(`📭 未找到 local.yaml(候选链均无此文件:${r.searched.slice(0, 3).join(' ; ')} …)`)
2677
+ console.error(' 注意:local.yaml 恒在 .sillyspec/ 下,项目根目录没有这个文件。')
2678
+ console.error(' 生成:sillyspec local detect(本地嗅探)/ sillyspec platform connect(平台凭据)')
2679
+ console.error(' 可用键清单:sillyspec config schema')
2680
+ process.exitCode = 1
2681
+ break
2682
+ }
2683
+ console.log(`📄 ${r.path}(来源:${r.source})`)
2684
+ const content = readFileSync(r.path, 'utf8')
2685
+ process.stdout.write(content)
2686
+ if (!content.endsWith('\n')) process.stdout.write('\n')
2687
+ break;
2688
+ }
2689
+ if (!isSchema) {
2690
+ const sug = didYouMean(configSub, ['schema', 'cat']);
2691
+ console.error(`❌ 未知子命令: config ${configSub}`);
2692
+ if (sug) console.error(` 你是想输入「config ${sug}」吗?`);
2693
+ console.error('用法: sillyspec config [schema|cat] [--json] 打印 local.yaml 配置键 / 读取实际内容');
2694
+ process.exit(2);
2695
+ }
2696
+ const { renderSchemaHuman, renderSchemaJson } = await import('./config-schema.js');
2697
+ if (json) console.log(renderSchemaJson());
2698
+ else console.log(renderSchemaHuman());
2699
+ break;
2700
+ }
2701
+ case 'runtime': {
2702
+ // D2:.runtime/ 产物索引。CLI 往 .runtime/ 写各种证据/状态文件(db、doctor-diagnosis、
2703
+ // workflow-runs、user-inputs…),却没有命令让 agent 一眼看到「我手上有哪些产物」。
2704
+ // 多会话/压缩后尤其痛:上轮写的证据文件,下轮不知去哪找。纯只读枚举,不写盘。
2705
+ const runtimeSubCmd = filteredArgs[1];
2706
+ if (runtimeSubCmd && runtimeSubCmd !== 'list') {
2707
+ const sug = didYouMean(runtimeSubCmd, ['list']);
2708
+ console.error(`❌ 未知子命令: runtime ${runtimeSubCmd}`);
2709
+ if (sug) console.error(` 你是想输入「runtime ${sug}」吗?`);
2710
+ console.error('用法: sillyspec runtime list 枚举 .sillyspec/.runtime/ 产物(只读)');
2711
+ process.exit(2);
2712
+ }
2713
+ const specRoot = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
2714
+ const runtimeDir = join(specRoot, '.runtime');
2715
+ if (!existsSync(runtimeDir)) {
2716
+ if (json) { console.log(JSON.stringify({ runtimeDir, exists: false, artifacts: [] }, null, 2)); break; }
2717
+ console.log(`📭 无 .runtime/ 目录(${runtimeDir})——尚未产生任何运行时产物。`);
2718
+ break;
2719
+ }
2720
+ // 已知产物 → 用途说明。agent 据此判断每份产物能否作为证据/该读哪个。
2721
+ const KNOWN = {
2722
+ 'sillyspec.db': '进度库 SQLite(stages/steps/changes 表)',
2723
+ 'sillyspec.db.bak': '进度库备份(sql.js 时代写前快照遗留,node:sqlite 不再写入,仅 _openWithFallback 向后兼容恢复用)',
2724
+ 'sillyspec.db.schema-version': '进度库 schema 版本标记',
2725
+ 'doctor-diagnosis.json': 'doctor --json 结构化诊断快照',
2726
+ 'doctor-dumps': 'doctor --dump-db 取证输出目录',
2727
+ 'workflow-runs': 'workflow check --save 归档目录',
2728
+ 'scan-guard.json': 'scan 覆盖保护记录',
2729
+ 'quick-sessions': 'quick 会话记录目录',
2730
+ 'quick-guard.json': 'quick baseline 守卫(旧位置)',
2731
+ 'worktrees': 'worktree meta(hooks 侧)',
2732
+ 'user-inputs.md': '步骤输出/输入历史追加日志',
2733
+ 'agent-session-log.json': '本地 agent 会话日志登记留底(run 命令自动 REST 上报平台 POST /api/agent-logs,见 docs/platform-agent-log-protocol.md)',
2734
+ 'current-quick-run-id': '当前 quick sessionId 指针(--done 无 --change 时 fallback)',
2735
+ 'scan-runs': '平台模式 scan/workflow 取证目录',
2736
+ 'artifacts': '派生产物目录(init 预建)',
2737
+ 'contract-artifacts': 'execute endpoint 契约产物(verify 阶段读取)',
2738
+ 'history': '历史记录目录(init 预建)',
2739
+ 'logs': '日志目录(init 预建)',
2740
+ 'templates': '工作流模板目录(init 预建)',
2741
+ };
2742
+ let entries;
2743
+ try {
2744
+ entries = readdirSync(runtimeDir, { withFileTypes: true });
2745
+ } catch (e) {
2746
+ console.error(`❌ 读取 .runtime/ 失败: ${e.message}`);
2747
+ process.exit(1);
2748
+ }
2749
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2750
+ if (json) {
2751
+ console.log(JSON.stringify({
2752
+ runtimeDir, exists: true,
2753
+ artifacts: entries.map(e => ({ name: e.name, type: e.isDirectory() ? 'dir' : 'file', desc: KNOWN[e.name] || null })),
2754
+ }, null, 2));
2755
+ break;
2756
+ }
2757
+ console.log(`📂 .runtime/ 产物(${runtimeDir})`);
2758
+ if (entries.length === 0) { console.log(' (空目录)'); break; }
2759
+ for (const e of entries) {
2760
+ const kind = e.isDirectory() ? '📁' : '📄';
2761
+ const desc = KNOWN[e.name] ? ` — ${KNOWN[e.name]}` : '';
2762
+ console.log(` ${kind} ${e.name}${desc}`);
2763
+ }
2764
+ const unknown = entries.filter(e => !KNOWN[e.name]);
2765
+ if (unknown.length > 0) {
2766
+ console.log(`\n (${unknown.length} 项未登记:${unknown.map(e => e.name).join(', ')})`);
2767
+ }
2768
+ break;
2769
+ }
2770
+ default: {
2771
+ const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'knowledge', 'platform', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
2772
+ const suggestion = didYouMean(command, topCommands);
2773
+ console.error(`❌ 未知命令: ${command}`);
2774
+ if (command === '--status') {
2775
+ // --status 是阶段内 flag(sillyspec run <stage> --status),裸用会落到 default
2776
+ console.error(' --status 是阶段内 flag。查看进度请用:sillyspec status 或 sillyspec progress show');
2777
+ } else if (suggestion) {
2778
+ console.error(` 你是想输入「${suggestion}」吗?`);
2779
+ }
2780
+ printUsage();
2781
+ process.exit(1);
2782
+ }
2783
+ }
2784
+ }
2785
+
2786
+ main().catch((err) => {
2787
+ // 平台指针失效:fail-closed,打印修复引导而非静默回退/stack trace
2788
+ if (err?.name === 'PointerUnreachableError') {
2789
+ console.error(`❌ ${err.message}`);
2790
+ process.exit(1);
2791
+ }
2792
+ // message 提首行(Agent 可直接解析 stderr 首行判断成败),堆栈降级为附件;
2793
+ // 用 exitCode 而非 exit(1) 让事件循环自然 drain,缓解 Windows libuv teardown
2794
+ // 把退出码污染成 127(与 shell "command not found" 撞码)的问题。
2795
+ console.error(`❌ SillySpec 发生错误:${err?.message || err}`);
2796
+ if (err?.stack) console.error(err.stack);
2797
+ process.exitCode = 1;
2798
+ });