sillyspec 3.27.2 → 3.27.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/eval/README.md +86 -0
  147. package/eval/eval.config.json +28 -0
  148. package/eval/lib/adapters/cli.mjs +63 -0
  149. package/eval/lib/adapters/dryrun.mjs +27 -0
  150. package/eval/lib/config.mjs +82 -0
  151. package/eval/lib/db.mjs +76 -0
  152. package/eval/lib/report.mjs +120 -0
  153. package/eval/lib/runner.mjs +157 -0
  154. package/eval/lib/tasks.mjs +73 -0
  155. package/eval/lib/util.mjs +107 -0
  156. package/eval/lib/workdir.mjs +30 -0
  157. package/eval/run.mjs +83 -0
  158. package/eval/tasks/001-slugify/files/package.json +6 -0
  159. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  160. package/eval/tasks/001-slugify/task.json +6 -0
  161. package/eval/tasks/001-slugify/verify.mjs +28 -0
  162. package/eval/tasks/002-each-day/files/package.json +6 -0
  163. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  164. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  165. package/eval/tasks/002-each-day/task.json +6 -0
  166. package/eval/tasks/002-each-day/verify.mjs +31 -0
  167. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  168. package/eval/tasks/003-word-report/files/package.json +6 -0
  169. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  170. package/eval/tasks/003-word-report/task.json +6 -0
  171. package/eval/tasks/003-word-report/verify.mjs +38 -0
  172. package/package.json +4 -2
  173. package/src/agent-session-log.js +45 -5
  174. package/src/change-risk-profile.js +83 -14
  175. package/src/config-schema.js +18 -3
  176. package/src/contract-matrix.js +90 -29
  177. package/src/datetime.js +20 -0
  178. package/src/decision-distill.js +517 -0
  179. package/src/dispatch/backends/local-agent.js +4 -4
  180. package/src/docs-check.js +227 -0
  181. package/src/docs-debt.js +187 -24
  182. package/src/doctor-diagnostics.js +102 -2
  183. package/src/foreign-declared.js +78 -0
  184. package/src/index.js +89 -9
  185. package/src/init.js +9 -7
  186. package/src/knowledge-match.js +107 -6
  187. package/src/local-register.js +7 -1
  188. package/src/module-resolve.js +12 -1
  189. package/src/modules.js +2 -1
  190. package/src/plan-adopt-waves.js +126 -0
  191. package/src/quicklog.js +50 -4
  192. package/src/run/command.js +122 -43
  193. package/src/run/complete-handlers.js +48 -19
  194. package/src/run/complete.js +5 -2
  195. package/src/run/gates.js +87 -22
  196. package/src/run/prompt.js +84 -42
  197. package/src/run/quick-audit.js +11 -0
  198. package/src/run/shared.js +152 -10
  199. package/src/run.js +2 -2
  200. package/src/scan-postcheck.js +30 -17
  201. package/src/stage-contract-spec.js +2 -1
  202. package/src/stage-contract.js +68 -4
  203. package/src/stages/archive.js +53 -12
  204. package/src/stages/brainstorm-auto.js +7 -0
  205. package/src/stages/brainstorm.js +11 -0
  206. package/src/stages/doctor.js +157 -1
  207. package/src/stages/execute.js +32 -6
  208. package/src/stages/plan-postcheck.js +76 -17
  209. package/src/stages/plan.js +1 -0
  210. package/src/stages/quick.js +6 -1
  211. package/src/stages/verify.js +31 -14
  212. package/src/sync.js +41 -3
  213. package/src/task-review.js +2 -2
  214. package/src/taskcard-placeholders.js +21 -0
  215. package/src/taskcard.js +10 -1
  216. package/src/verify-postcheck.js +315 -13
  217. package/src/verify-probes.js +46 -4
  218. package/src/worktree-apply.js +184 -7
  219. package/src/worktree-deps.js +57 -0
  220. package/src/worktree.js +201 -58
  221. package/templates/prompts/taskcard-rules.md +1 -0
@@ -0,0 +1,843 @@
1
+ /**
2
+ * run/prompt.js(W6 Step3 从 run.js 抽出)。
3
+ *
4
+ * prompt 渲染主干:outputStep(步骤 prompt 组装 + 占位符替换 + 平台/scanProfile/
5
+ * 模块上下文/铁律注入)+ applyRootPlaceholders({SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
6
+ * {WORKFLOWS_ROOT}/{KNOWLEDGE_ROOT} 路径根占位符替换,平台/常规模式共用)+
7
+ * loadModuleContextIndex/buildModuleContextInjection/parseModuleMapSimple
8
+ * (_module-map.yaml 模块上下文匹配注入,仅 outputStep 用)。
9
+ *
10
+ * 安全锚:run.js 始终 barrel。applyRootPlaceholders 由 run.js import 回来;
11
+ * outputStep(output-step-render / archive-task-completion-injection 测试)+ applyRootPlaceholders
12
+ * (prompt-placeholders 测试)被 test 直接 import 本模块(outputStep 不经 barrel,2026-08-13 起 test
13
+ * 直 import 源模块,`_outputStepForTest` 别名已移除)。
14
+ *
15
+ * 路径修正(相对 src/run/):
16
+ * - stageRegistry 从 '../stages/index.js';resolvePromptIncludes/safeGit/WAIT_MARKER_RE 从 './shared.js'
17
+ * - 动态 import './knowledge-match.js'/'./task-review.js'/'./review-tier.js'/'./stage-review.js' → '../X.js'(src/ 下,退一层)
18
+ * - 删除 outputStep 内死代码 `const { execSync } = await import('child_process')`(execSync 解构未用,实际走 safeGit)
19
+ * - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
20
+ */
21
+ import { basename, join } from 'node:path'
22
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
23
+ import { stageRegistry } from '../stages/index.js'
24
+ import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
25
+ import { renderStageContract } from '../stage-contract-spec.js'
26
+ import { nowWallClock } from '../datetime.js'
27
+ import { parseModuleMapSimple } from '../modules.js'
28
+ import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
29
+
30
+ /**
31
+ * 从 _module-map.yaml 读取模块上下文索引
32
+ * 用于 brainstorm/plan/execute 阶段按任务命中模块精准注入上下文
33
+ *
34
+ * @param {string} specBase - 规范目录(.sillyspec 或 specRoot)
35
+ * @param {string} projectName - 项目名
36
+ * @returns {object|null} 解析后的模块索引,null 表示无索引
37
+ */
38
+ export function loadModuleContextIndex(specBase, projectName) {
39
+ try {
40
+ const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
41
+ if (!existsSync(mapPath)) return null
42
+ const content = readFileSync(mapPath, 'utf8')
43
+ // schema_version 校验:仅缺失 schema_version(真 malformed,parseModuleMapSimple 必错位)时 warn;
44
+ // schema_version=1(旧格式)静默——读端 buildModuleContextInjection 已 v1/v2 双兼容
45
+ // (data.paths || data.core_files),v1 解析正常。原 v1 warn 是过激噪声(每步渲染 prompt 刷屏)。
46
+ // v1→v2 根因在 scan prompt 模板仍写 schema_version:1,升级 scan.js 是单独改动(见 troubleshooting)。
47
+ const sv = content.match(/^schema_version:\s*(\d+)/m)
48
+ if (!sv) {
49
+ console.warn(`⚠️ _module-map.yaml 缺少 schema_version 声明(期望 2),模块解析可能错位:${mapPath}(跑 \`sillyspec modules rebuild\` 升级到 schema_version: 2 可消除此警告)`)
50
+ }
51
+ return parseModuleMapSimple(content)
52
+ } catch {
53
+ return null
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 根据 AI 输出的任务描述,匹配相关模块并生成上下文注入文本
59
+ * 匹配策略:模块 id / role / doc 路径中的关键词
60
+ *
61
+ * @param {string} taskDescription - 任务描述(来自 plan.md / step prompt / outputText)
62
+ * @param {object} moduleIndex - loadModuleContextIndex 返回值
63
+ * @param {string} specBase - 规范目录
64
+ * @param {string} projectName - 项目名
65
+ * @returns {string} 上下文注入文本,空字符串表示无匹配模块
66
+ */
67
+ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) {
68
+ if (!moduleIndex || !taskDescription) return ''
69
+
70
+ const taskLower = taskDescription.toLowerCase()
71
+ const matched = []
72
+
73
+ for (const [moduleId, data] of Object.entries(moduleIndex)) {
74
+ let score = 0
75
+ let matchReasons = []
76
+ // 模块 id 匹配
77
+ if (taskLower.includes(moduleId.toLowerCase())) { score += 3; matchReasons.push(`id:${moduleId}`) }
78
+ // role 描述匹配
79
+ if (data.role && taskLower.includes(data.role.toLowerCase())) { score += 2; matchReasons.push('role') }
80
+ // core_files 路径匹配
81
+ const coreFiles = data.paths || data.core_files || []
82
+ for (const p of coreFiles) {
83
+ if (taskLower.includes(p.toLowerCase())) { score += 1; matchReasons.push(`file:${p}`); break }
84
+ }
85
+ if (score > 0) matched.push({ moduleId, data, score, matchReasons })
86
+ }
87
+
88
+ if (matched.length === 0) return ''
89
+
90
+ matched.sort((a, b) => b.score - a.score)
91
+
92
+ let injection = '\n### 📦 模块上下文(按相关性排序,来自 Module Context Index)\n\n'
93
+ injection += `> 以下模块上下文由 scan 阶段生成的 _module-map.yaml 自动匹配。\n`
94
+ injection += `> Matched modules: ${matched.map(m => m.moduleId).join(', ')}\n`
95
+ injection += `> Reasons: ${matched.map(m => m.matchReasons.join(', ')).join('; ')}\n\n`
96
+
97
+ for (const { moduleId, data } of matched) {
98
+ injection += `#### ${moduleId}\n`
99
+ if (data.role) injection += `- **职责**: ${String(data.role).slice(0, 100)}\n`
100
+ const riskLevel = data.risk_level || 'medium'
101
+ injection += `- **风险等级**: ${riskLevel}\n`
102
+ const coreFiles = data.paths || data.core_files || []
103
+ if (coreFiles.length > 0) injection += `- **核心文件**: ${coreFiles.join(', ')}\n`
104
+ if (data.doc) {
105
+ const docPath = join(specBase, 'docs', projectName, data.doc)
106
+ const exists = existsSync(docPath)
107
+ injection += `- **模块文档**: ${data.doc}${exists ? ' ✅' : ' ⚠️ 不存在'}\n`
108
+ }
109
+ const deps = data.depends_on || []
110
+ if (deps.length > 0) injection += `- **依赖**: ${deps.join(', ')}\n`
111
+ const usedBy = data.used_by || []
112
+ if (usedBy.length > 0) injection += `- **被引用**: ${usedBy.join(', ')}\n`
113
+ injection += '\n'
114
+ }
115
+
116
+ return injection
117
+ }
118
+
119
+ // parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
120
+ // 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
121
+ /**
122
+ * 提示词路径根的单一解析:specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)。
123
+ * 治 execute review.json 提示路径写 worktree 副本、--done 校验读主仓的分裂(坑 execute-prompt-spec-drift):
124
+ * CLI 自动锚定主仓 spec(command.js detectWorktreeSpecDrift → platformOpts.specDriftAnchor)只修正了
125
+ * resolveRuntimeRoot 的 runtime 落点,outputStep 拼 {SPEC_ROOT}/.runtime/execute-runs/… 若仍用 cwd
126
+ * (worktree)→ 提示的 review.json 路径落到 worktree 副本 .sillyspec,而 gate/checkbox 从主仓
127
+ * resolveRuntimeRoot 读 → agent 落盘错位,marker 漂移改用他 run / task 未勾。specDriftAnchor 非空即主仓
128
+ * specBase,优先于 cwd。平台模式 specRoot 优先语义不变。
129
+ */
130
+ export function resolvePromptSpecBase(platformOpts, cwd) {
131
+ return platformOpts?.specRoot || platformOpts?.specDriftAnchor || join(cwd, '.sillyspec')
132
+ }
133
+
134
+ /**
135
+ * 输出当前步骤的 prompt
136
+ */
137
+ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null) {
138
+ const step = steps[stepIndex]
139
+ const total = steps.length
140
+ // ── 越界防御 ──
141
+ // steps/defSteps 来自 getStageSteps(如 buildPlanSteps),其长度可能与 progress.steps
142
+ // 不一致(例如平台模式下 changeDir 解析失败导致 buildPlanSteps(null) 只返回 2 步 fixedPrefix)。
143
+ // 此时 stepIndex 可能越界,访问 step.name 会 TypeError 崩溃。降级处理而非崩溃。
144
+ if (!step) {
145
+ console.error(`⚠️ 无法输出步骤 ${stepIndex + 1}/${total}:步骤定义缺失`)
146
+ console.error(` stage=${stageName} stepIndex=${stepIndex} defSteps.length=${steps.length}`)
147
+ console.error(` 可能原因:平台模式下 getStageSteps 未传 specRoot,或 plan/execute 步骤动态生成后 defSteps 未同步。`)
148
+ console.error(` 请检查 --change 指定的变更目录是否存在、specRoot 是否正确,然后重试。`)
149
+ return false
150
+ }
151
+ const projectName = dbProjectName || basename(cwd)
152
+
153
+ // ── Revision context injection ──
154
+ const revisionCtx = platformOpts?._revision
155
+ if (revisionCtx) {
156
+ console.log(`### 🔄 Revision Context`)
157
+ console.log(`本阶段处于修订模式(revision ${revisionCtx.revision}),不是首次执行。`)
158
+ console.log(`- 修订起始步骤:${revisionCtx.fromStep}`)
159
+ console.log(`- 当前步骤之前已完成的步骤仍然有效,不需要重做。`)
160
+ console.log(`- 当前步骤及之后的步骤需要重新生成或调整已有产物。`)
161
+ console.log(`- 已有产物文件(design.md、plan.md 等)被保留,审视并更新它们,而不是从零创建。`)
162
+ console.log(`- 不要绕过 CLI 进度追踪。\n`)
163
+ }
164
+
165
+ const personas = {
166
+ brainstorm: `### 🎯 你的角色:资深架构师
167
+ 你是一位有 15 年经验的系统架构师。先理解业务本质,再设计技术方案。决策附理由,方案列 trade-off。不确定就说不确定,不猜。`,
168
+ plan: `### 📋 你的角色:技术项目经理
169
+ 你是一位经验丰富的技术项目经理。任务拆解粒度均匀,依赖关系明确。每个任务有完成标准,Wave 间有依赖说明。条理清晰,不做模糊描述。`,
170
+ execute: `### 💻 你的角色:高级工程师
171
+ 你是一位严谨的高级工程师。先读规范再写代码,严格遵循 CONVENTIONS.md 和 plan.md。**你不是设计师,是执行者——按 plan 搬砖,禁止发散思维。** 发现 plan 不合理就停下来反馈,不要自己改方案。代码有清晰职责划分,边界处理完善。少说多做,遇到规范冲突优先问。`,
172
+ verify: `### 🔍 你的角色:QA 专家
173
+ 你是一位吹毛求疵的 QA 专家。假设所有代码都有 bug,用最坏情况测试。关注边界、异常、并发。有问题直说,用证据说话,不写"看起来没问题"。`,
174
+ quick: `### 💻 你的角色:全栈老兵
175
+ 你是一位实战经验丰富的全栈工程师。不纠结架构和流程,理解需求就直接干。不确定的地方先问清楚再动手,先读后写,改完就收。问题排查思路开阔,前端报错不一定是前端问题——可能是后端数据、浏览器兼容、甚至设备硬件。解决方案实用接地气,用户描述有误敢于直接指出。`,
176
+ explore: `### 🧭 你的角色:技术探索伙伴
177
+ 你帮助用户澄清问题、调查代码库、比较方案和暴露风险。探索阶段不写实现代码,不安装依赖,不把讨论强行推进成开发。`
178
+ }
179
+
180
+ console.log(`---`)
181
+ console.log(`stage: ${stageName}`)
182
+ console.log(`step: ${stepIndex + 1}/${total}`)
183
+ console.log(`stepName: ${step.name}`)
184
+ console.log(`project: ${projectName}`)
185
+ if (changeName) {
186
+ console.log(`change: ${changeName}`)
187
+ // changeDir 根(坑 change-dir-base-mismatch):平台/漂移锚定用绝对根(specDriftAnchor 防
188
+ // worktree 副本错位),普通本地保持相对 '.sillyspec' 展示契约(test 钉死)。此前
189
+ // isPlatform 用 specRoot||runtimeRoot 判定、取值只拿 specRoot——runtimeRoot-only 平台组合
190
+ // join(null,...) 直接 TypeError。
191
+ const changeDirBase = platformOpts?.specRoot || platformOpts?.specDriftAnchor || '.sillyspec'
192
+ const changeDir = join(changeDirBase, 'changes', changeName)
193
+ console.log(`changeDir: ${changeDir}`)
194
+ }
195
+ console.log(`---\n`)
196
+ // persona 只在 stage 首步注入(step0)——角色设定一次即可,后续 step 重复注入纯属 token 浪费
197
+ if (personas[stageName] && stepIndex === 0) {
198
+ console.log(personas[stageName])
199
+ console.log('')
200
+ }
201
+ // 注入全局护栏(如 _globalGuardrails)
202
+ const stageDef = stageRegistry[stageName]
203
+ const guardrails = stageDef && stageDef._globalGuardrails ? stageDef._globalGuardrails : ''
204
+
205
+ console.log(`## Step ${stepIndex + 1}/${total}: ${step.name}\n`)
206
+ if (guardrails) {
207
+ if (stepIndex === 0) {
208
+ console.log(guardrails.trim())
209
+ console.log('')
210
+ } else {
211
+ // 护栏已在首步全文注入并留在 context;后续步只留一行精简提醒——
212
+ // 防 context 压缩后 agent 遗忘安全约束(如 verify 禁破坏性 git/源码操作),又避免每步重复 ~1KB。
213
+ console.log(`⛔ 本阶段护栏生效中(禁止破坏性操作,详见首步护栏)\n`)
214
+ }
215
+ }
216
+ // 先解析 {{include: name}}(把外部模板片段拉进 prompt),再做下方占位符替换,
217
+ // 保证模板内容里的 {SPEC_ROOT}/<change-name> 等也能被替换
218
+ let promptText = resolvePromptIncludes(step.prompt)
219
+ // 替换 prompt 中的占位符
220
+ if (projectName && promptText.includes('<project>')) {
221
+ promptText = promptText.replace(/<project>/g, projectName)
222
+ }
223
+ // 替换 <git-user> 占位符
224
+ if (promptText.includes('<git-user>')) {
225
+ try {
226
+ const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
227
+ promptText = promptText.replace(/<git-user>/g, gitUser)
228
+ } catch {
229
+ promptText = promptText.replace(/<git-user>/g, 'unknown')
230
+ }
231
+ }
232
+ // 替换时间戳占位符(本地墙钟统一走 nowWallClock——坑 taskcard-created-at-utc:人读字段用
233
+ // UTC 会偏一个时区,子代理手工改)
234
+ const now = new Date()
235
+ const nowDatetime = nowWallClock(now)
236
+ const nowTimestamp = now.getFullYear() + String(now.getMonth()+1).padStart(2,'0') + String(now.getDate()).padStart(2,'0') + '-' + String(now.getHours()).padStart(2,'0') + String(now.getMinutes()).padStart(2,'0') + String(now.getSeconds()).padStart(2,'0')
237
+ const nowDate = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0')
238
+ promptText = promptText.replace(/<now-datetime>/g, nowDatetime)
239
+ promptText = promptText.replace(/<now-timestamp>/g, nowTimestamp)
240
+ promptText = promptText.replace(/<now-date>/g, nowDate)
241
+ // <now-iso-datetime>(scan frontmatter updated_at 用;与 scan-fix-headers 同款秒级格式)。
242
+ // 占位符名带 iso 是历史遗留——值已统一为本地墙钟(同 <now-datetime>),改名会破坏 stages 引用。
243
+ promptText = promptText.replace(/<now-iso-datetime>/g, nowWallClock(now))
244
+ // <git-head-short>(scan frontmatter source_commit 用;CLI 代查,agent 勿自跑 rev-parse)
245
+ if (promptText.includes('<git-head-short>')) {
246
+ let headShort = 'unknown'
247
+ try {
248
+ headShort = safeGit(cwd, ['rev-parse', '--short', 'HEAD']).value || 'unknown'
249
+ } catch { /* git 不可用 → unknown,scan-postcheck 失配时再核 */ }
250
+ promptText = promptText.replace(/<git-head-short>/g, headShort)
251
+ }
252
+ // 替换 {REVIEW_SCHEMA_VERSION} 占位符(task review 示例模板用,值=CLI 当前 REVIEW_SCHEMA_VERSION 常量,
253
+ // 避免 agent 照抄 design 目标版本与 CLI 写侧常量漂移;与 stage review 契约 renderReviewJsonContract 动态注入同源)
254
+ if (promptText.includes('{REVIEW_SCHEMA_VERSION}')) {
255
+ promptText = promptText.replace(/\{REVIEW_SCHEMA_VERSION\}/g, String(REVIEW_SCHEMA_VERSION))
256
+ }
257
+ // 替换 <change-name> 占位符
258
+ if (changeName && promptText.includes('<change-name>')) {
259
+ promptText = promptText.replace(/<change-name>/g, changeName)
260
+ }
261
+ // 替换 <quick-session-id> 占位符(quick 阶段专用:sessionId == changeName == quick-<uuid8>,
262
+ // 见 runStage 参数解析 quickSessionId 生成。告知 agent 本会话 id + --done 需带 --change)
263
+ if (changeName && promptText.includes('<quick-session-id>')) {
264
+ promptText = promptText.replace(/<quick-session-id>/g, changeName)
265
+ }
266
+ // 替换 <quicklog-id> 占位符(quick 阶段:从 session guard.json 读 CLI 分配的 ql-ID,
267
+ // 供 agent 在模块文档变更索引等处引用)
268
+ if (promptText.includes('<quicklog-id>')) {
269
+ const specBaseQl = resolvePromptSpecBase(platformOpts, cwd)
270
+ let qlIdVal = ''
271
+ try {
272
+ const sessionGuardFile = join(specBaseQl, '.runtime', 'quick-sessions', changeName, 'guard.json')
273
+ if (existsSync(sessionGuardFile)) {
274
+ qlIdVal = JSON.parse(readFileSync(sessionGuardFile, 'utf8')).quicklogId || ''
275
+ }
276
+ } catch (err) {
277
+ console.warn(`⚠️ quicklog-id 占位符读取 guard.json 失败: ${err && err.message ? err.message : err}`);
278
+ }
279
+ promptText = promptText.replace(/<quicklog-id>/g, qlIdVal || '(未分配)')
280
+ }
281
+ // 替换 <linked-changes> 占位符(quick 阶段:从 .runtime/quick-sessions/<sessionId>/guard.json 读关联变更)
282
+ if (promptText.includes('<linked-changes>')) {
283
+ const specBaseLc = resolvePromptSpecBase(platformOpts, cwd)
284
+ let linkedChanges = []
285
+ try {
286
+ // D-002:guard 按 session 存。changeName == quick-<uuid8>(见 runStage 参数解析)。回退读旧单文件(兼容 task-03 前)
287
+ const sessionGuardFile = join(specBaseLc, '.runtime', 'quick-sessions', changeName, 'guard.json')
288
+ const legacyGuardFile = join(specBaseLc, '.runtime', 'quick-guard.json')
289
+ const guard = existsSync(sessionGuardFile)
290
+ ? JSON.parse(readFileSync(sessionGuardFile, 'utf8'))
291
+ : (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
292
+ if (guard) linkedChanges = Array.isArray(guard.linkedChanges) ? guard.linkedChanges : []
293
+ } catch {}
294
+ const display = linkedChanges.length > 0 ? linkedChanges.join(', ') : '(无)'
295
+ promptText = promptText.replace(/<linked-changes>/g, display)
296
+ }
297
+ // 平台模式:注入路径覆盖指令
298
+ if (platformOpts?.specRoot || platformOpts?.runtimeRoot) {
299
+ const projectName = dbProjectName || basename(cwd)
300
+ // platformOpts.specRoot 现在指向 specDir 本身(可能是 cwd/.sillyspec 或外部路径)
301
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
302
+ const docsRoot = join(specSillyspec, 'docs', projectName)
303
+ const projectsRoot = join(specSillyspec, 'projects')
304
+ const changesRoot = join(specSillyspec, 'changes')
305
+ const workflowsRoot = join(specSillyspec, 'workflows')
306
+ const knowledgeRoot = join(specSillyspec, 'knowledge')
307
+
308
+ promptText = applyRootPlaceholders(promptText, { specRoot: specSillyspec, docsRoot, projectsRoot, workflowsRoot, knowledgeRoot })
309
+
310
+ const platformDirectives = []
311
+ platformDirectives.push(
312
+ `## ⚠️ 平台模式 — 写入路径约束(必须严格遵守)\n` +
313
+ `\n` +
314
+ `规范目录(specDir): \`${specSillyspec}\`\n` +
315
+ `- 文档根目录: \`${docsRoot}/\`\n` +
316
+ `- 项目注册表: \`${projectsRoot}/\`\n` +
317
+ `- 变更目录: \`${changesRoot}/\`\n` +
318
+ `- 工作流目录: \`${workflowsRoot}/\`\n` +
319
+ `- 术语目录: \`${knowledgeRoot}/\`\n` +
320
+ `\n` +
321
+ `### ⛔ 写入规则\n` +
322
+ `1. **所有文档、配置、产物只能写入上述路径**。严禁写入源码目录或相对路径 \`.sillyspec/\`。\n` +
323
+ `2. **不允许**从 cwd 推导文档路径,必须使用上面列出的绝对路径。\n` +
324
+ `3. **源码扫描范围**必须排除:.sillyspec/、.claude/、.git/、node_modules/、dist/、build/、__pycache__/\n` +
325
+ `4. **local.yaml 校验**:commands 中引用的命令必须在 package.json 的 scripts 中存在,不存在的标记为 unavailable,不能写 "配置良好"\n` +
326
+ `\n` +
327
+ `### ⛔ Write 工具规则\n` +
328
+ `1. 如果 Write 返回 \"File has not been read yet\",正确动作是:先 Read 目标文件 → 再 Write 覆盖。\n` +
329
+ `2. **不允许**用 cat >、tee、heredoc 等 Bash 方式绕过 Write 工具。\n` +
330
+ `3. 如果 Write 和 Read 均失败,记录失败并停止当前 step。\n` +
331
+ `\n` +
332
+ `### 📍 Workflow YAML 占位符映射(task-05)\n` +
333
+ `读取 \`{WORKFLOWS_ROOT}/scan-docs.yaml\` 时,yaml 内的占位符按以下映射替换为绝对路径:\n` +
334
+ `- \`{SPEC_ROOT}\` → \`${specSillyspec}\`(规范目录根)\n` +
335
+ `- \`<project>\` → 当前项目名(见下方 step 提示,等于 \`${projectName}\`)\n` +
336
+ `- 例:\`{SPEC_ROOT}/docs/<project>/scan/ARCHITECTURE.md\` → \`${docsRoot}/scan/ARCHITECTURE.md\`\n` +
337
+ `\n` +
338
+ `创建目录: \`mkdir -p ${docsRoot}/{scan,modules,flows} ${projectsRoot} ${changesRoot}\`\n`
339
+ )
340
+ if (platformOpts.runtimeRoot) {
341
+ const scanRunId = platformOpts.scanRunId || 'scan-' + new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')
342
+ platformDirectives.push(
343
+ `运行时产物写入: \`${platformOpts.runtimeRoot}/scan-runs/${scanRunId}/\`\n`
344
+ )
345
+ }
346
+ if (platformOpts.workspaceId) {
347
+ platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
348
+ }
349
+ // 平台 directives 前置条件:
350
+ // - step0(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射一次建立即可;
351
+ // - scan 阶段所有 step:scan 的 profileDirectives 本就每步注入(见下方 L917),平台路径
352
+ // 约束也需每步可见——否则 quick profile 的 step0 是 noAI preflight(自动执行),run scan
353
+ // 输出的首个可见 step 是 step1,会漏掉 specDir 路径,导致平台模式写入约束丢失。
354
+ // 后续 step 靠 changeDir header + footer 平台规则(L1077+)维持,platformDirectives
355
+ // 每步重复约 500 tokens 仅在 scan(步数有限)可接受。
356
+ if (stepIndex === 0 || stageName === 'scan') {
357
+ promptText = platformDirectives.join('\n') + '\n\n' + promptText
358
+ }
359
+ } else {
360
+ // 常规模式(无平台 specRoot):占位符替换为 cwd/.sillyspec 下对应路径
361
+ // 让用 {SPEC_ROOT}/{DOCS_ROOT} 等占位符的 prompt(如 quick/scan)在常规模式也写到正确位置
362
+ const projectName = dbProjectName || basename(cwd)
363
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
364
+ promptText = applyRootPlaceholders(promptText, {
365
+ specRoot: specSillyspec,
366
+ docsRoot: join(specSillyspec, 'docs', projectName),
367
+ projectsRoot: join(specSillyspec, 'projects'),
368
+ workflowsRoot: join(specSillyspec, 'workflows'),
369
+ knowledgeRoot: join(specSillyspec, 'knowledge'),
370
+ })
371
+ }
372
+
373
+ // 注入 scanProfile 硬约束指令
374
+ if (stageName === 'scan' && platformOpts?.scanProfile) {
375
+ const sp = platformOpts.scanProfile
376
+ const profileDirectives = []
377
+ profileDirectives.push(`## 📊 Scan Profile: ${sp.mode} (${sp.reason})`)
378
+ if (sp.maxAgentCalls === 0) {
379
+ profileDirectives.push(`**⛔ 严禁使用子代理(Agent/Task 工具)。** 必须在本 turn 内完成所有工作。`)
380
+ } else if (sp.maxAgentCalls > 0) {
381
+ profileDirectives.push(`**子代理上限:${sp.maxAgentCalls} 个。** 不要超出。`)
382
+ }
383
+ if (sp.maxDocs < 99) {
384
+ profileDirectives.push(`**文档上限:${sp.maxDocs} 份。** 只生成核心文档,不要额外生成 flows/glossary/module-card。`)
385
+ }
386
+ profileDirectives.push(`--output 只需要列出文件名,不要写长篇总结。`)
387
+ promptText = profileDirectives.join('\n') + '\n\n' + promptText
388
+
389
+ // scanProfile 分支也要替换占位符(非 platform 模式也会走到这里)
390
+ const _pName = dbProjectName || basename(cwd)
391
+ const _specSS = resolvePromptSpecBase(platformOpts, cwd)
392
+ const _docsRoot = join(_specSS, 'docs', _pName)
393
+ promptText = applyRootPlaceholders(promptText, {
394
+ specRoot: _specSS,
395
+ docsRoot: _docsRoot,
396
+ projectsRoot: join(_specSS, 'projects'),
397
+ workflowsRoot: join(_specSS, 'workflows'),
398
+ knowledgeRoot: join(_specSS, 'knowledge'),
399
+ })
400
+ } else {
401
+ // 非 platform 模式也要替换占位符
402
+ const projectName = dbProjectName || basename(cwd)
403
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
404
+ const docsRoot = join(specSillyspec, 'docs', projectName)
405
+ const projectsRoot = join(specSillyspec, 'projects')
406
+ const workflowsRoot = join(specSillyspec, 'workflows')
407
+ const knowledgeRoot = join(specSillyspec, 'knowledge')
408
+ promptText = applyRootPlaceholders(promptText, { specRoot: specSillyspec, docsRoot, projectsRoot, workflowsRoot, knowledgeRoot })
409
+ }
410
+
411
+ // scan 漂移事实注入(债单 D-7 方案 A):brainstorm「加载项目上下文」读 scan 文档前,
412
+ // CLI 算出 staleness(source_commit vs HEAD)注入一行事实——"算事实注入"原则(债单第六节),
413
+ // 不是劝说指令。computeScanStaleness 内部全降级(无文档 null / git 失败 unknown),不阻断。
414
+ if (stageName === 'brainstorm' && promptText.includes('{SCAN_STALENESS}')) {
415
+ try {
416
+ const { computeScanStaleness } = await import('../scan-staleness.js')
417
+ const stalenessSpecBase = resolvePromptSpecBase(platformOpts, cwd)
418
+ const projectName = dbProjectName || basename(cwd)
419
+ const fact = computeScanStaleness({ projectRoot: cwd, specBase: stalenessSpecBase, projectName })
420
+ const injected = fact
421
+ ? `[docs-debt] scan 基线漂移:${fact.message}`
422
+ : '[docs-debt] scan 基线漂移:无 scan 文档(绿地项目),跳过判定'
423
+ promptText = promptText.replace(/\{SCAN_STALENESS\}/g, injected)
424
+ } catch (e) {
425
+ promptText = promptText.replace(/\{SCAN_STALENESS\}/g, `[docs-debt] scan 漂移检测异常(${e.message}),跳过判定`)
426
+ }
427
+ }
428
+
429
+ // 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
430
+ // {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
431
+ // (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
432
+ // runtime JSON(与 KNOWLEDGE_HIT_REPORT 的 json 落盘同口径)。advisory 展示,不改变步骤流程;
433
+ // 全降级不抛(异常 → 单行说明)。
434
+ if (stageName === 'brainstorm' && promptText.includes('{DECISION_HITS}')) {
435
+ try {
436
+ const { matchKnowledge } = await import('../knowledge-match.js')
437
+ const decSpecBase = resolvePromptSpecBase(platformOpts, cwd)
438
+ const decKnowledgeDir = join(decSpecBase, 'knowledge')
439
+ // taskContext:changeName(brainstorm Step2 时 tasks.md 尚未生成,变更名是唯一稳定任务
440
+ // 语料;与 execute 分支的 changeName 基底同口径)
441
+ const decResult = matchKnowledge(decKnowledgeDir, changeName || '')
442
+ const rejectedHits = (decResult.decisionHits || []).filter(h => h.status === 'rejected')
443
+ let decInjected = ''
444
+ if (rejectedHits.length > 0) {
445
+ const lines = [
446
+ '⚠️ 否决决策提示(历史已否决,防复潮)——下列决策此前已被否决。看到否决理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1)说明依据:'
447
+ ]
448
+ for (const h of rejectedHits) {
449
+ lines.push(`- ${h.id} ${h.title}(${h.file})`)
450
+ lines.push(` - 否决理由:${h.reason || '(未记录)'}`)
451
+ lines.push(` - 复潮条件:${h.revisitWhen || '(未记录)'}`)
452
+ }
453
+ decInjected = '\n\n' + lines.join('\n')
454
+ }
455
+ promptText = promptText.replace(/\{DECISION_HITS\}/g, decInjected)
456
+ // 注入结果同步落 runtime JSON(与既有 KNOWLEDGE_HIT_REPORT 落盘口径一致)
457
+ const decRuntimeDir = join(decSpecBase, '.runtime')
458
+ mkdirSync(decRuntimeDir, { recursive: true })
459
+ writeFileSync(join(decRuntimeDir, 'decision-hits.json'), JSON.stringify({ matched: decResult.matched, decisionHits: decResult.decisionHits || [] }, null, 2) + '\n')
460
+ } catch (e) {
461
+ promptText = promptText.replace(/\{DECISION_HITS\}/g, `[decisions] 决策命中检测异常(${e.message}),跳过`)
462
+ }
463
+ }
464
+
465
+ // Knowledge hit report: execute 阶段注入匹配结果
466
+ if (stageName === 'execute' && promptText.includes('{KNOWLEDGE_HIT_REPORT}')) {
467
+ try {
468
+ const { matchKnowledge } = await import('../knowledge-match.js')
469
+ const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
470
+ const knowledgeDir = join(effectiveSpecBase, 'knowledge')
471
+ // taskContext: changeName + tasks.md 任务名(注册表唯一真相,2026-08-20-task-truth-unify)
472
+ let taskContext = changeName || ''
473
+ if (changeName) {
474
+ const tasksMdPath = join(effectiveSpecBase, 'changes', changeName, 'tasks.md')
475
+ const planPath = join(effectiveSpecBase, 'changes', changeName, 'plan.md')
476
+ const srcPath = existsSync(tasksMdPath) ? tasksMdPath : planPath
477
+ try {
478
+ const registryContent = readFileSync(srcPath, 'utf8')
479
+ // match "- [ ] task-01: title" / "## task-01: title"(兼容旧 plan.md 内联两种形态)
480
+ const taskLines = [...registryContent.matchAll(/(?:^\- \[[ x]\] |^## )task-\d+[^:]*:?\s*(.+)/gm)]
481
+ if (taskLines.length > 0) {
482
+ taskContext += ' ' + taskLines.map(t => t[1]).join(' ')
483
+ }
484
+ } catch {}
485
+ }
486
+ const knowledgeResult = matchKnowledge(knowledgeDir, taskContext)
487
+ promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, knowledgeResult.report)
488
+ // 写入 runtime JSON
489
+ const runtimeDir = join(effectiveSpecBase, '.runtime')
490
+ mkdirSync(runtimeDir, { recursive: true })
491
+ writeFileSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeResult.json, null, 2) + '\n')
492
+ } catch (e) {
493
+ promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
494
+ }
495
+ }
496
+
497
+ // docs-debt 模块文档欠账事实注入(2026-08-15 docs-debt-inject D-006,债单第六节"CLI 算事实"):
498
+ // Wave prompt {DOCS_DEBT} → computeDocsDebt(git diff × module-map)facts;空 facts 替换为空串
499
+ // (无债零输出,无残留占位符)。changedFiles 口径(D-001/FR-007):worktree 根
500
+ // (meta.json 所在)git status --porcelain + diff baselineCommit..HEAD 并集——公共实现已抽至
501
+ // docs-debt.collectExecuteChangedFiles(execute.js Wave 步渲染点同源复用,勿双写)。
502
+ // 全降级不抛(异常 → 单行说明)。
503
+ if (stageName === 'execute' && promptText.includes('{DOCS_DEBT}')) {
504
+ try {
505
+ const debtSpecBase = resolvePromptSpecBase(platformOpts, cwd)
506
+ const debtProjectName = dbProjectName || basename(cwd)
507
+ const { collectExecuteChangedFiles, computeDocsDebt, computeDecisionTouches, renderDecisionTouchFacts } = await import('../docs-debt.js')
508
+ const { root: debtProjectRoot, changedFiles } = collectExecuteChangedFiles({ specBase: debtSpecBase, changeName, cwd })
509
+ const result = computeDocsDebt({
510
+ projectRoot: debtProjectRoot,
511
+ specBase: debtSpecBase,
512
+ projectName: debtProjectName,
513
+ changedFiles,
514
+ })
515
+ // 决策锚点触碰事实(2026-08-24-decision-touch-cli-drift task-01,W-A 次渲染点):重入/reset
516
+ // 场景 changedFiles 非空时呈现。advisory 追加渲染分支——既有 docs-debt facts 逐字不变,
517
+ // 无触碰 render 返回 ''(零输出);主渲染点在 execute.js buildWavePrompt(同源 facts 计算)。
518
+ let debtTouchFacts = ''
519
+ try {
520
+ debtTouchFacts = renderDecisionTouchFacts(computeDecisionTouches(changedFiles, join(debtSpecBase, 'knowledge')).touches)
521
+ } catch { /* 触碰计算降级为空——不影响欠账 facts 注入 */ }
522
+ promptText = promptText.replace(/\{DOCS_DEBT\}/g, [result.facts, debtTouchFacts].filter(Boolean).join('\n'))
523
+ } catch (e) {
524
+ promptText = promptText.replace(/\{DOCS_DEBT\}/g, `[docs-debt] 欠账计算异常(${e.message}),跳过`)
525
+ }
526
+ }
527
+
528
+ // 模块卡分级表注入(token 成本优化 P0a,2026-08-22-token-cost-optimization):execute
529
+ // 「加载上下文」步 {MODULE_RESOLVE_TABLE} → resolveChangeModuleCards(tasks 卡 allowed_paths
530
+ // × 全部 _module-map.yaml 跨层最长前缀匹配,子项目细卡优先于根层大卡)。全降级不抛
531
+ // (异常 → 单行说明),无 map 由渲染函数给出跳过提示——占位符恒被替换。
532
+ if (stageName === 'execute' && promptText.includes('{MODULE_RESOLVE_TABLE}')) {
533
+ try {
534
+ const { renderModuleResolveTable } = await import('../module-resolve.js')
535
+ const mrSpecBase = resolvePromptSpecBase(platformOpts, cwd)
536
+ const table = renderModuleResolveTable({ cwd, specBase: mrSpecBase, changeName })
537
+ promptText = promptText.replace(/\{MODULE_RESOLVE_TABLE\}/g, table)
538
+ } catch (e) {
539
+ promptText = promptText.replace(/\{MODULE_RESOLVE_TABLE\}/g, `[modules-resolve] 模块卡分级解析异常(${e.message}),跳过——可手跑 sillyspec modules resolve --change <变更名>`)
540
+ }
541
+ }
542
+
543
+ // Execute: 注入 currentExecuteRunId(从变更专属标记文件读取)
544
+ if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
545
+ let runId = ''
546
+ const execSpecBase = resolvePromptSpecBase(platformOpts, cwd)
547
+ const runtimeRoot = resolveRuntimeRoot(platformOpts, execSpecBase)
548
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
549
+ try {
550
+ if (existsSync(runIdFile)) {
551
+ const c = readFileSync(runIdFile, 'utf8').trim()
552
+ // marker 是 agent 可写内容,直接注入 prompt + 拼进 gate 读 review.json 的路径——
553
+ // 格式校验同时防提示词注入与路径穿越(对齐 stage-review marker 的前缀校验范式)
554
+ if (c && !isValidExecuteRunId(c)) {
555
+ console.warn(`[sillyspec] execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),视为缺失重新生成`)
556
+ } else {
557
+ runId = c
558
+ }
559
+ }
560
+ } catch {}
561
+ if (!runId) {
562
+ const { generateExecuteRunId } = await import('../task-review.js')
563
+ runId = generateExecuteRunId()
564
+ // 落盘(与启动站点一致),保证 agent 收到的 ID == gate/checkbox 读取的 ID
565
+ // D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在)。
566
+ // 渲染路径异常不能炸 prompt 输出 → catch 内 console.error 留痕 + 保留降级(ID 注入继续,
567
+ // 目录由下游 review 写入 / 其他写入点补齐)。
568
+ try {
569
+ mkdirSync(join(runtimeRoot, 'execute-runs', runId, 'tasks'), { recursive: true })
570
+ writeFileSync(runIdFile, runId + '\n')
571
+ } catch (e) {
572
+ console.error(`[sillyspec] execute run marker/目录写入失败(降级继续,ID ${runId} 仍注入 prompt): ${e.message}`)
573
+ }
574
+ }
575
+ promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
576
+ }
577
+
578
+ // Stage Review Tier:brainstorm/plan/execute 阶段注入审查分级占位符
579
+ // (scanProfile 只在 scan 生效、change-risk-profile 只管 apply/verify,都不约束这些阶段的审查方式,
580
+ // 故按 plan_level / 变更文件数分级:self 当前 agent 自审,independent 强制独立子代理 + review.json)
581
+ if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
582
+ try {
583
+ const { classifyReviewTier } = await import('../review-tier.js')
584
+ const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
585
+ const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
586
+ const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
587
+ const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
588
+ let planLevel = null
589
+ if (tierChangeDir) {
590
+ const planPath = join(tierChangeDir, 'plan.md')
591
+ if (existsSync(planPath)) {
592
+ const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
593
+ if (fmLine) planLevel = fmLine.split(':')[1].trim()
594
+ }
595
+ }
596
+ const tier = classifyReviewTier({ planLevel, designPath })
597
+ // reviewRunId:优先读 marker 复用(保证多次渲染 prompt 注入同一 ID == gate 读取的 ID,
598
+ // 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),marker 缺失才
599
+ // generate + 落盘。对齐 execute {EXECUTE_RUN_ID} 段(prompt.js:449-467)。
600
+ const tierRuntimeRoot = resolveRuntimeRoot(platformOpts, tierSpecBase)
601
+ const reviewRunIdMarker = stageReviewMarkerPath(tierRuntimeRoot, stageName, changeName)
602
+ let reviewRunId = ''
603
+ try {
604
+ if (existsSync(reviewRunIdMarker)) {
605
+ reviewRunId = readFileSync(reviewRunIdMarker, 'utf8').trim()
606
+ }
607
+ } catch {}
608
+ if (!reviewRunId) {
609
+ reviewRunId = generateStageReviewRunId()
610
+ try {
611
+ mkdirSync(tierRuntimeRoot, { recursive: true })
612
+ writeFileSync(reviewRunIdMarker, reviewRunId + '\n')
613
+ // plan-c: echo 完整 review 目录路径,避免 agent 拿裸 runId 给子代理时漏连字符拼错路径
614
+ const reviewDir = `${tierRuntimeRoot}/stage-reviews/${stageName}-${reviewRunId}`
615
+ console.log(` 📁 Stage Review 写入目录(直接复制给 review 子代理,勿手拼 runId):${reviewDir}/`)
616
+ } catch {}
617
+ }
618
+ // review.json 产物契约(schema + 示例 + docHash 算法 + 重算提示)事前注入,与 validateStageReviewSchema 同源
619
+ const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier })
620
+ promptText = promptText
621
+ .split('{REVIEW_TIER}').join(tier.tier)
622
+ .split('{REVIEW_TIER_REASON}').join(tier.reason)
623
+ .split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
624
+ .split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
625
+ } catch (e) {
626
+ // 降级 self,避免 prompt 残留占位符
627
+ promptText = promptText
628
+ .split('{REVIEW_TIER}').join('self')
629
+ .split('{REVIEW_TIER_REASON}').join('分级异常降级 self: ' + e.message)
630
+ .split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
631
+ .split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
632
+ }
633
+ }
634
+
635
+ // archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
636
+ // 替代「机械数 plan.md checkbox」(checkbox 依赖 autoCheckPlanFromReviews 回填,runId marker /
637
+ // review 缺失时回填静默 no-op,会停在未勾态导致完成度失真 → archive 误判「全未完成」)。
638
+ // summarizeTaskCompletion 内部已 fail-safe 降级(无 plan / 无 runId → checkbox 统计 + 标注 source),
639
+ // 此处仅兜底注入异常,绝不阻断 archive。
640
+ if (stageName === 'archive' && promptText.includes('{TASK_COMPLETION_REPORT}')) {
641
+ try {
642
+ const { summarizeTaskCompletion } = await import('../task-review.js')
643
+ const tcrSpecBase = resolvePromptSpecBase(platformOpts, cwd)
644
+ const tcrRuntimeRoot = resolveRuntimeRoot(platformOpts, tcrSpecBase)
645
+ const tcrChangeDir = changeName ? join(tcrSpecBase, 'changes', changeName) : null
646
+ const summary = tcrChangeDir
647
+ ? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
648
+ : null
649
+ promptText = promptText.split('{TASK_COMPLETION_REPORT}').join(summary ? summary.report : '(无法计算完成度:变更目录缺失)')
650
+ } catch (e) {
651
+ promptText = promptText.split('{TASK_COMPLETION_REPORT}').join('(完成度计算异常:' + e.message + ',请手动核对 plan.md 与 .runtime/execute-runs/*/tasks/task-NN/review.json)')
652
+ }
653
+ }
654
+
655
+ // verify Step2「加载规范并锚定」worktree 基线锚点注入:execute 在 worktree 分支上 commit,
656
+ // verify 的 diff 对账基点 = worktree 分支的真实基点(git merge-base),不是主仓当前 HEAD。
657
+ // agent 拿不到基点时会拿主仓 HEAD / 旧 release commit 兜底——主仓在 execute 期间推进时
658
+ // (并行 session commit)会把别人的演进误判为本变更越权新增(2026-08-17 实录:误用 d192f89
659
+ // 而实际 merge-base 49ade71,第 6/7 条铁律被误判新增)。注入 fail-soft:worktree/分支/meta
660
+ // 读不到时降级为自查指引,绝不阻断 verify。
661
+ if (stageName === 'verify' && promptText.includes('{WORKTREE_BASELINE_INFO}')) {
662
+ let baselineInfo
663
+ try {
664
+ const { WorktreeManager } = await import('../worktree.js')
665
+ const wm = new WorktreeManager({ cwd })
666
+ const meta = changeName ? wm.getMeta(changeName) : null
667
+ const branch = meta?.branch || (changeName ? 'sillyspec/' + changeName : null)
668
+ const branchExists = branch
669
+ ? safeGit(cwd, ['rev-parse', '--verify', '--quiet', branch + '^{commit}']).error === null
670
+ : false
671
+ if (branchExists) {
672
+ const baseBranch = meta?.baseBranch || 'main'
673
+ const mergeBase = safeGit(cwd, ['merge-base', baseBranch, branch]).value
674
+ baselineInfo =
675
+ `- 本变更 worktree 分支:${branch}(存在)\n` +
676
+ (meta
677
+ ? `- worktree 基点(创建时锚定):baseHash=${meta.baseHash || '未知'},actualBaseHash=${meta.actualBaseHash || '未知'}(含 dirty overlay 后的 worktree 起点)\n`
678
+ : '- worktree meta 已不可读(可能已被 cleanup),用下方 merge-base 即可\n') +
679
+ `- ⚠️ diff 对账基点:\`git merge-base ${baseBranch} ${branch}\` = ${mergeBase || '(计算失败,手动跑上面命令)'}\n` +
680
+ ` 不要用主仓当前 HEAD / 最近 release commit 当基点——主仓在 execute 期间可能已被并行 session 推进,用错基点会把别人的演进误判为本变更越权改动。\n` +
681
+ `- task review 的 base/head 引用分支上的 commit,逐 task 核验 diff 用 \`git diff <base>..<head>\`(分支 ref 若已被 cleanup 删除,hash 仍可直达,但不要主动删分支)。`
682
+ } else if (meta) {
683
+ baselineInfo =
684
+ '- 本变更 worktree meta 存在但分支不可达(可能已被 cleanup):按主仓 git log 对照本变更各 commit 核验;task review 的 base/head 若悬空,在 verify-result.md 标注审计链风险。'
685
+ } else {
686
+ baselineInfo =
687
+ '- 本变更无 worktree(in-place / 纯文档变更 / worktree 已清理且 meta 不可读):按主仓 git log 最近提交对照核验。'
688
+ }
689
+ } catch (e) {
690
+ baselineInfo = '(worktree 基线注入失败:' + e.message + '——diff 对账前先 git branch -a 查 sillyspec/<change> 分支并用 git merge-base <main> <branch> 自查基点,勿用主仓 HEAD 当基点)'
691
+ }
692
+ promptText = promptText.split('{WORKTREE_BASELINE_INFO}').join(baselineInfo)
693
+ }
694
+
695
+ // verify「运行测试和质量扫描」evidence-auto 推荐注入(task-12,FR-11 / D-005@v2):
696
+ // verify-postcheck 在 --done 事后解析 evidence-auto,agent 写 verify-result.md 时看不到
697
+ // 推荐组合、无从否决——故在 prompt 时点经 {EVIDENCE_AUTO_RECOMMENDATION} 把 task-11
698
+ // resolveTestStrategy 的 evidence_auto_recommendation.summary 渲染进 step prompt(含推荐
699
+ // 理由、降级注记与「可在 verify-result.md 否决并改跑全量」路径),供用户否决(FR-11)。
700
+ // 仅 test_strategy=evidence-auto 且 recommendation 非空时渲染;full/module/skip/未配置
701
+ // (recommendation=null)替换为空串零输出。fail-soft:读取/解析异常降级单行说明不抛,
702
+ // 绝不阻断 verify prompt 输出(对齐上方 {WORKTREE_BASELINE_INFO} 注入先例)。
703
+ if (stageName === 'verify' && promptText.includes('{EVIDENCE_AUTO_RECOMMENDATION}')) {
704
+ let eaInjected = ''
705
+ try {
706
+ const { resolveTestStrategy } = await import('../verify-postcheck.js')
707
+ const eaSpecBase = resolvePromptSpecBase(platformOpts, cwd)
708
+ // 信任边界:local.yaml 只读 specBase(平台 specRoot / 漂移锚定主仓 / cwd 本地),与
709
+ // runVerifyTestCheck 的 specBase 来源同口径,不读 agent 可写的 worktree 副本。
710
+ const eaYamlPath = join(eaSpecBase, 'local.yaml')
711
+ const eaYamlText = existsSync(eaYamlPath) ? readFileSync(eaYamlPath, 'utf8') : null
712
+ const eaChangeDir = changeName ? join(eaSpecBase, 'changes', changeName) : null
713
+ const resolution = resolveTestStrategy({ yamlText: eaYamlText, changeDir: eaChangeDir })
714
+ const rec = resolution.evidence_auto_recommendation
715
+ if (rec && rec.summary) eaInjected = rec.summary
716
+ } catch (e) {
717
+ eaInjected = `(evidence-auto 推荐注入失败:${e.message}——请读 .sillyspec/local.yaml 确认 test_strategy;--done 时 CLI 仍会按 resolveTestStrategy 实测对账,推荐不可用时可显式设 test_strategy: full/module)`
718
+ }
719
+ promptText = promptText.split('{EVIDENCE_AUTO_RECOMMENDATION}').join(eaInjected)
720
+ }
721
+
722
+ // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
723
+ if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
724
+ const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
725
+ const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
726
+ if (moduleIndex && Object.keys(moduleIndex).length > 0) {
727
+ // 尝试从 step prompt / changeName 匹配模块
728
+ const taskDesc = step.prompt || changeName || ''
729
+ const injection = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName)
730
+ if (injection) {
731
+ promptText = injection + '\n' + promptText
732
+ }
733
+ }
734
+ }
735
+
736
+ // 平台模式 prompt 自检:确保没有裸相对输出路径
737
+ // 只匹配正向写入指令中的裸路径,避免误杀「禁止写入 .sillyspec/」等安全说明
738
+ if ((platformOpts?.specRoot || platformOpts?.runtimeRoot) && stageName === 'scan') {
739
+ const writeCtx = /(?<!不要|禁止|严禁)(?:save[\s.]+to|write|create|mkdir|git add|写入|保存到|写入到)[^a-zA-Z]*\.sillyspec\/[a-z]/i
740
+ if (writeCtx.test(promptText)) {
741
+ console.error(`❌ [sillyspec] BUG: 平台模式 scan prompt 包含写入指令指向裸相对路径 .sillyspec/`)
742
+ console.error(` 这会导致 agent 写入源码目录而非 spec-root,属于源码污染 bug。`)
743
+ console.error(` 请将路径改为对应的 {DOCS_ROOT}/{PROJECTS_ROOT}/{WORKFLOWS_ROOT}/{KNOWLEDGE_ROOT}/{SPEC_ROOT} 占位符。`)
744
+ process.exit(2) // 内部异常(SillySpec 自身 prompt 配置 bug)→ exit 2,与 machine-interface 三段契约一致
745
+ }
746
+ }
747
+
748
+ if (prevStepAnswer) {
749
+ console.log(`\n### 📩 上一步用户回答`)
750
+ console.log(prevStepAnswer)
751
+ }
752
+
753
+ // 完成契约(事前预知):该 stage 的机械校验通过条件,从 stage-contract-spec.js manifest 渲染,
754
+ // 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅 step0 注入(与 persona/guardrails 同模式,
755
+ // 一次建立即可;后续步靠 context 保留)。
756
+ if (stepIndex === 0) {
757
+ const stageContract = renderStageContract(stageName)
758
+ if (stageContract) {
759
+ promptText = `${promptText}\n\n${stageContract}`
760
+ }
761
+ }
762
+
763
+ console.log(promptText)
764
+ // 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首步注入——
765
+ // 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
766
+ // 且依赖 changeName/platformOpts,必须【每步注入】(context 压缩丢失会让 agent 越界写源码)。
767
+ if (stepIndex === 0) {
768
+ console.log(`\n### ⚠️ 铁律`)
769
+ console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
770
+ console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
771
+ console.log('- 已完成步骤视为只读:需修订用 `sillyspec run <stage> --reopen --from-step N` 回退重做,直接回头改会让进度记录与产物脱节。')
772
+ console.log('- CLI 子命令以本 prompt 或上一条 --done 输出的字面为准;不确定时停下问用户,猜测命令会让状态机推进到错误位置。')
773
+ console.log('- 本步骤产物落盘后立即执行 prompt 末尾的 --done:CLI 据此校验产出并推进状态机;不跑则进度永远停在本步。')
774
+ console.log('- 变更目录改名用 `sillyspec change-rename <旧名> <新名>`:mv/rename 会漏改进度库引用,导致变更失联。')
775
+ console.log('- 文档类型文件(.md/.yaml/.json 等)头部必须包含 author(git 用户名)和 created_at(精确到秒)')
776
+ console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
777
+ }
778
+ // 平台模式 + 路径规则(安全关键,每步注入;step1+ 起带精简标题,不复述通用铁律)
779
+ if (platformOpts?.specRoot || platformOpts?.runtimeRoot || changeName) {
780
+ if (stepIndex !== 0) console.log(`\n### ⚠️ 路径与平台规则(每步提醒,通用铁律见首步)`)
781
+ if (platformOpts?.specRoot || platformOpts?.runtimeRoot) {
782
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
783
+ console.log(`- **平台模式:所有文件只能写入 \`${specSillyspec}/\` 下的对应子目录,严禁写入源码目录。**`)
784
+ console.log('- **平台模式:Write 工具失败时,不允许用 cat > / tee / heredoc 等方式绕过。先 Read 再 Write,仍失败则记录并停止。**')
785
+ console.log('- **平台模式:local.yaml 中的 commands 必须在 package.json scripts 中真实存在,不存在的标记 unavailable。**')
786
+ }
787
+ if (changeName) {
788
+ // changeDir 根与上方头部同源(坑 change-dir-base-mismatch):平台/漂移锚定绝对根,
789
+ // 普通本地保持相对 '.sillyspec' 展示;runtimeRoot-only 组合不再 join(null) 崩溃
790
+ const changeDirBase = platformOpts?.specRoot || platformOpts?.specDriftAnchor || '.sillyspec'
791
+ const changeDir = join(changeDirBase, 'changes', changeName)
792
+ // quick 会话(quick-<hex8>)按设计无实体变更目录(progress.js initChange 同款跳过),
793
+ // 纯代码 quick 根本不产 spec 文档——「所有变更文件必须写入 changes/<change>/」的硬规则
794
+ // 对它是误导(ql-20260821-011 实证:CLI 反复提示一个用不到的目录)。改述条件化:
795
+ // 仅「若本 quick 决定补文档」才落该目录,代码改动照常写源码目录。
796
+ if (QUICK_SID_RE.test(changeName)) {
797
+ console.log(`- **文件路径规则(quick 会话):纯代码改动直接写源码目录,无目录限制。仅当本 quick 需要落 spec 文档(design/plan 等,复杂任务建议升级完整流程)时才写 \`${changeDir}/\`;QUICKLOG/tasks.md 记录由 CLI 接管,不要手建。**`)
798
+ } else {
799
+ console.log(`- **文件路径规则:所有变更文件必须写入 \`${changeDir}/\` 目录下。不要自己拼接路径,直接使用 changeDir 值。示例:\`${changeDir}/proposal.md\`**`)
800
+ }
801
+ }
802
+ }
803
+ const changeFlag = changeName ? ` --change ${changeName}` : ''
804
+ // 检测当前 step prompt 是否包含 WAIT 指令(即可能需要等待用户)
805
+ const stepPrompt = promptText || ''
806
+ const requiresWait = step.requiresWait === true
807
+ const conditionalWait = step.conditionalWait === true
808
+ const mayNeedWait = WAIT_MARKER_RE.test(stepPrompt) || requiresWait || conditionalWait
809
+
810
+ console.log(`\n### 完成后执行`)
811
+ if (requiresWait) {
812
+ console.log(`本步骤必须等待用户输入,不能直接 --done:`)
813
+ console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
814
+ console.log(``)
815
+ console.log(`用户回答后执行:`)
816
+ console.log(`sillyspec run ${stageName} --continue --answer "用户回答"${changeFlag}`)
817
+ console.log(``)
818
+ console.log(`收到回答并完成本步骤总结后,再执行:`)
819
+ } else if (mayNeedWait) {
820
+ console.log(`如果需要用户决策(选择方案/确认设计等):`)
821
+ console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
822
+ console.log(``)
823
+ console.log(`如果不需要用户决策,正常完成:`)
824
+ }
825
+ console.log(`sillyspec run ${stageName} --done${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
826
+ }
827
+ /**
828
+ * 替换 prompt 文本中的路径根占位符 {SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
829
+ * {WORKFLOWS_ROOT}/{KNOWLEDGE_ROOT}。平台模式与常规模式共用:仅传入的 roots 值不同。
830
+ * 占位符值都是绝对路径、互不包含其它占位符,故替换顺序不影响结果。
831
+ *
832
+ * 注:本函数由并行会话引入(outputStep 两处逐字重复逻辑的抽取),重构期间 run.js 一次
833
+ * git checkout 误覆盖其未提交版本,此处按 test/prompt-placeholders.test.mjs 的契约重建,
834
+ * 行为等价。
835
+ */
836
+ export function applyRootPlaceholders(text, roots) {
837
+ return text
838
+ .replaceAll('{SPEC_ROOT}', roots.specRoot)
839
+ .replaceAll('{DOCS_ROOT}', roots.docsRoot)
840
+ .replaceAll('{PROJECTS_ROOT}', roots.projectsRoot)
841
+ .replaceAll('{WORKFLOWS_ROOT}', roots.workflowsRoot)
842
+ .replaceAll('{KNOWLEDGE_ROOT}', roots.knowledgeRoot)}
843
+