prizmkit 1.1.99 → 1.1.101

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 (321) hide show
  1. package/bin/create-prizmkit.js +4 -4
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/dev-pipeline/.env.example +3 -3
  4. package/bundled/dev-pipeline/README.md +110 -90
  5. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
  6. package/bundled/dev-pipeline/cli.py +27 -0
  7. package/bundled/dev-pipeline/prizmkit_runtime/__init__.py +13 -0
  8. package/bundled/dev-pipeline/prizmkit_runtime/__main__.py +9 -0
  9. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +193 -0
  10. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +313 -0
  11. package/bundled/dev-pipeline/prizmkit_runtime/compat.py +32 -0
  12. package/bundled/dev-pipeline/prizmkit_runtime/config.py +410 -0
  13. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +478 -0
  14. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +734 -0
  15. package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +256 -0
  16. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +109 -0
  17. package/bundled/dev-pipeline/prizmkit_runtime/paths.py +110 -0
  18. package/bundled/dev-pipeline/prizmkit_runtime/processes.py +373 -0
  19. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +389 -0
  20. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +51 -0
  21. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +179 -0
  22. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +345 -0
  23. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +118 -0
  24. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +187 -0
  25. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +147 -0
  26. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +422 -0
  27. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +552 -0
  28. package/bundled/dev-pipeline/prizmkit_runtime/status.py +71 -0
  29. package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
  30. package/bundled/dev-pipeline/scripts/continuation.py +374 -0
  31. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +64 -52
  32. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +35 -25
  33. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +2 -2
  34. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
  35. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +185 -16
  36. package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
  37. package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
  38. package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
  39. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +8 -2
  40. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +4 -0
  41. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -5
  42. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +16 -6
  43. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +16 -6
  44. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  45. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  46. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -0
  47. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +13 -69
  48. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +15 -7
  49. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +42 -29
  50. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +41 -39
  51. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +4 -0
  52. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +4 -0
  53. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  54. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  55. package/bundled/dev-pipeline/tests/test_auto_skip.py +932 -0
  56. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
  57. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +100 -0
  58. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
  59. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +1138 -0
  60. package/bundled/dev-pipeline/tests/test_unified_cli.py +1045 -0
  61. package/bundled/skills/_metadata.json +1 -1
  62. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +29 -34
  63. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +6 -5
  64. package/bundled/skills/feature-pipeline-launcher/SKILL.md +29 -35
  65. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +7 -6
  66. package/bundled/skills/feature-workflow/SKILL.md +3 -6
  67. package/bundled/skills/recovery-workflow/SKILL.md +23 -32
  68. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +29 -35
  69. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +7 -6
  70. package/bundled/skills/refactor-workflow/SKILL.md +3 -6
  71. package/bundled/templates/hooks/commit-intent.json +2 -2
  72. package/bundled/templates/hooks/diff-prizm-docs.py +226 -0
  73. package/bundled/templates/hooks/prizm-pre-commit.py +45 -0
  74. package/bundled/templates/hooks/validate-prizm-docs.py +240 -0
  75. package/package.json +1 -1
  76. package/src/config.js +33 -12
  77. package/src/index.js +3 -6
  78. package/src/manifest.js +3 -2
  79. package/src/metadata.js +7 -18
  80. package/src/prompts.js +0 -12
  81. package/src/runtimes.js +18 -11
  82. package/src/scaffold.js +133 -116
  83. package/src/upgrade.js +1 -1
  84. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -500
  85. package/bundled/dev-pipeline/launch-feature-daemon.sh +0 -657
  86. package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -502
  87. package/bundled/dev-pipeline/lib/branch.sh +0 -326
  88. package/bundled/dev-pipeline/lib/common.sh +0 -1257
  89. package/bundled/dev-pipeline/lib/heartbeat.sh +0 -469
  90. package/bundled/dev-pipeline/reset-bug.sh +0 -395
  91. package/bundled/dev-pipeline/reset-feature.sh +0 -406
  92. package/bundled/dev-pipeline/reset-refactor.sh +0 -393
  93. package/bundled/dev-pipeline/run-bugfix.sh +0 -1315
  94. package/bundled/dev-pipeline/run-feature.sh +0 -1602
  95. package/bundled/dev-pipeline/run-recovery.sh +0 -648
  96. package/bundled/dev-pipeline/run-refactor.sh +0 -1348
  97. package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
  98. package/bundled/dev-pipeline/tests/test-path-resolution.sh +0 -100
  99. package/bundled/dev-pipeline-windows/.env.example +0 -36
  100. package/bundled/dev-pipeline-windows/README.md +0 -30
  101. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -533
  102. package/bundled/dev-pipeline-windows/assets/feature-list-example.json +0 -146
  103. package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -137
  104. package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +0 -9
  105. package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +0 -9
  106. package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +0 -9
  107. package/bundled/dev-pipeline-windows/lib/branch.ps1 +0 -235
  108. package/bundled/dev-pipeline-windows/lib/common.ps1 +0 -829
  109. package/bundled/dev-pipeline-windows/lib/daemon.ps1 +0 -140
  110. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +0 -967
  111. package/bundled/dev-pipeline-windows/lib/reset.ps1 +0 -87
  112. package/bundled/dev-pipeline-windows/reset-bug.ps1 +0 -9
  113. package/bundled/dev-pipeline-windows/reset-feature.ps1 +0 -9
  114. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +0 -9
  115. package/bundled/dev-pipeline-windows/run-bugfix.ps1 +0 -9
  116. package/bundled/dev-pipeline-windows/run-feature.ps1 +0 -9
  117. package/bundled/dev-pipeline-windows/run-recovery.ps1 +0 -179
  118. package/bundled/dev-pipeline-windows/run-refactor.ps1 +0 -9
  119. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +0 -228
  120. package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +0 -530
  121. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +0 -2282
  122. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +0 -767
  123. package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +0 -805
  124. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +0 -845
  125. package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +0 -316
  126. package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +0 -134
  127. package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +0 -382
  128. package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +0 -399
  129. package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +0 -102
  130. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +0 -1232
  131. package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +0 -191
  132. package/bundled/dev-pipeline-windows/scripts/prizmkit-test-gate.py +0 -446
  133. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +0 -913
  134. package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +0 -173
  135. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +0 -1558
  136. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +0 -1119
  137. package/bundled/dev-pipeline-windows/scripts/utils.py +0 -542
  138. package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +0 -7
  139. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +0 -7
  140. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +0 -65
  141. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +0 -5
  142. package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +0 -7
  143. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +0 -73
  144. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +0 -589
  145. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +0 -710
  146. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +0 -791
  147. package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +0 -263
  148. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +0 -326
  149. package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +0 -237
  150. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +0 -340
  151. package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +0 -270
  152. package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +0 -7
  153. package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +0 -91
  154. package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +0 -34
  155. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +0 -10
  156. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +0 -12
  157. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +0 -7
  158. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +0 -8
  159. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +0 -9
  160. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +0 -6
  161. package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +0 -21
  162. package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +0 -21
  163. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +0 -78
  164. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +0 -283
  165. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +0 -112
  166. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +0 -168
  167. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +0 -82
  168. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +0 -75
  169. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +0 -23
  170. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +0 -14
  171. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +0 -19
  172. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +0 -63
  173. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +0 -42
  174. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +0 -44
  175. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +0 -59
  176. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +0 -39
  177. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +0 -27
  178. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +0 -27
  179. package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +0 -39
  180. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +0 -35
  181. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +0 -35
  182. package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +0 -73
  183. package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +0 -13
  184. package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +0 -21
  185. package/bundled/dev-pipeline-windows/templates/sections/session-context.md +0 -5
  186. package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +0 -6
  187. package/bundled/dev-pipeline-windows/templates/sections/task-contract.md +0 -34
  188. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +0 -48
  189. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +0 -48
  190. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +0 -83
  191. package/bundled/skills-windows/app-planner/SKILL.md +0 -296
  192. package/bundled/skills-windows/app-planner/assets/app-design-guide.md +0 -101
  193. package/bundled/skills-windows/app-planner/references/architecture-decisions.md +0 -52
  194. package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +0 -101
  195. package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +0 -71
  196. package/bundled/skills-windows/app-planner/references/infrastructure-convention-discovery.md +0 -108
  197. package/bundled/skills-windows/app-planner/references/project-brief-guide.md +0 -82
  198. package/bundled/skills-windows/app-planner/references/project-conventions-discovery.md +0 -59
  199. package/bundled/skills-windows/app-planner/references/project-state-detection.md +0 -90
  200. package/bundled/skills-windows/app-planner/references/red-team-checklist.md +0 -40
  201. package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +0 -619
  202. package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +0 -285
  203. package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +0 -249
  204. package/bundled/skills-windows/app-planner/references/rules/backend/question-manifest.json +0 -46
  205. package/bundled/skills-windows/app-planner/references/rules/backend/template.md +0 -173
  206. package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +0 -382
  207. package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +0 -211
  208. package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +0 -184
  209. package/bundled/skills-windows/app-planner/references/rules/database/question-manifest.json +0 -39
  210. package/bundled/skills-windows/app-planner/references/rules/database/template.md +0 -158
  211. package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +0 -820
  212. package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +0 -188
  213. package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +0 -319
  214. package/bundled/skills-windows/app-planner/references/rules/frontend/question-manifest.json +0 -51
  215. package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +0 -339
  216. package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +0 -649
  217. package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +0 -290
  218. package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +0 -232
  219. package/bundled/skills-windows/app-planner/references/rules/mobile/question-manifest.json +0 -47
  220. package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +0 -175
  221. package/bundled/skills-windows/app-planner/references/rules-configuration.md +0 -46
  222. package/bundled/skills-windows/bug-fix-workflow/SKILL.md +0 -379
  223. package/bundled/skills-windows/bug-fix-workflow/references/bug-diagnosis.md +0 -41
  224. package/bundled/skills-windows/bug-planner/SKILL.md +0 -402
  225. package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +0 -43
  226. package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +0 -44
  227. package/bundled/skills-windows/bug-planner/references/error-recovery.md +0 -73
  228. package/bundled/skills-windows/bug-planner/references/input-formats.md +0 -53
  229. package/bundled/skills-windows/bug-planner/references/schema-validation.md +0 -25
  230. package/bundled/skills-windows/bug-planner/references/severity-rules.md +0 -16
  231. package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +0 -322
  232. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +0 -304
  233. package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +0 -84
  234. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +0 -381
  235. package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +0 -67
  236. package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
  237. package/bundled/skills-windows/feature-planner/SKILL.md +0 -397
  238. package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +0 -64
  239. package/bundled/skills-windows/feature-planner/assets/planning-guide.md +0 -214
  240. package/bundled/skills-windows/feature-planner/references/browser-interaction.md +0 -59
  241. package/bundled/skills-windows/feature-planner/references/completeness-review.md +0 -57
  242. package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +0 -75
  243. package/bundled/skills-windows/feature-planner/references/error-recovery.md +0 -90
  244. package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +0 -112
  245. package/bundled/skills-windows/feature-planner/references/new-project-planning.md +0 -85
  246. package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +0 -1020
  247. package/bundled/skills-windows/feature-workflow/SKILL.md +0 -374
  248. package/bundled/skills-windows/feature-workflow/references/brainstorm-guide.md +0 -137
  249. package/bundled/skills-windows/prizm-kit/SKILL.md +0 -93
  250. package/bundled/skills-windows/prizmkit-code-review/SKILL.md +0 -161
  251. package/bundled/skills-windows/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
  252. package/bundled/skills-windows/prizmkit-code-review/references/review-report-template.md +0 -31
  253. package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +0 -66
  254. package/bundled/skills-windows/prizmkit-code-review/scripts/check_loop.py +0 -186
  255. package/bundled/skills-windows/prizmkit-committer/SKILL.md +0 -89
  256. package/bundled/skills-windows/prizmkit-deploy/SKILL.md +0 -444
  257. package/bundled/skills-windows/prizmkit-deploy/references/ci-cd-workflows.md +0 -115
  258. package/bundled/skills-windows/prizmkit-deploy/references/cloud-platform-deploy.md +0 -93
  259. package/bundled/skills-windows/prizmkit-deploy/references/data-safety-examples.md +0 -120
  260. package/bundled/skills-windows/prizmkit-deploy/references/database-setup.md +0 -46
  261. package/bundled/skills-windows/prizmkit-deploy/references/deploy-config-schema.md +0 -148
  262. package/bundled/skills-windows/prizmkit-deploy/references/deploy-history-schema.md +0 -62
  263. package/bundled/skills-windows/prizmkit-deploy/references/deployment-modes.md +0 -50
  264. package/bundled/skills-windows/prizmkit-deploy/references/direct-upload.md +0 -26
  265. package/bundled/skills-windows/prizmkit-deploy/references/dns-setup.md +0 -42
  266. package/bundled/skills-windows/prizmkit-deploy/references/docker-deploy.md +0 -31
  267. package/bundled/skills-windows/prizmkit-deploy/references/firewall-setup.md +0 -37
  268. package/bundled/skills-windows/prizmkit-deploy/references/live-validation-notes.md +0 -21
  269. package/bundled/skills-windows/prizmkit-deploy/references/nginx-blue-green.md +0 -59
  270. package/bundled/skills-windows/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
  271. package/bundled/skills-windows/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
  272. package/bundled/skills-windows/prizmkit-deploy/references/ssh-takeover.md +0 -20
  273. package/bundled/skills-windows/prizmkit-deploy/references/ssl-setup.md +0 -56
  274. package/bundled/skills-windows/prizmkit-implement/SKILL.md +0 -71
  275. package/bundled/skills-windows/prizmkit-init/SKILL.md +0 -356
  276. package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +0 -82
  277. package/bundled/skills-windows/prizmkit-init/references/config-schema.md +0 -70
  278. package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +0 -41
  279. package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +0 -13
  280. package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +0 -9
  281. package/bundled/skills-windows/prizmkit-plan/SKILL.md +0 -102
  282. package/bundled/skills-windows/prizmkit-plan/assets/plan-template.md +0 -115
  283. package/bundled/skills-windows/prizmkit-plan/assets/spec-template.md +0 -73
  284. package/bundled/skills-windows/prizmkit-plan/references/clarify-guide.md +0 -67
  285. package/bundled/skills-windows/prizmkit-plan/references/examples.md +0 -85
  286. package/bundled/skills-windows/prizmkit-plan/references/verification-checklist.md +0 -60
  287. package/bundled/skills-windows/prizmkit-prizm-docs/SKILL.md +0 -129
  288. package/bundled/skills-windows/prizmkit-prizm-docs/assets/prizm-docs-format.md +0 -613
  289. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-init.md +0 -45
  290. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-rebuild.md +0 -15
  291. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-status.md +0 -14
  292. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-update.md +0 -19
  293. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-validate.md +0 -17
  294. package/bundled/skills-windows/prizmkit-retrospective/SKILL.md +0 -87
  295. package/bundled/skills-windows/prizmkit-retrospective/references/knowledge-injection-steps.md +0 -50
  296. package/bundled/skills-windows/prizmkit-retrospective/references/structural-sync-steps.md +0 -43
  297. package/bundled/skills-windows/prizmkit-test/SKILL.md +0 -227
  298. package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +0 -220
  299. package/bundled/skills-windows/prizmkit-test/references/examples.md +0 -143
  300. package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +0 -225
  301. package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +0 -109
  302. package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +0 -111
  303. package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +0 -347
  304. package/bundled/skills-windows/recovery-workflow/SKILL.md +0 -355
  305. package/bundled/skills-windows/recovery-workflow/evals/evals.json +0 -46
  306. package/bundled/skills-windows/recovery-workflow/references/detection.md +0 -58
  307. package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +0 -544
  308. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +0 -346
  309. package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +0 -72
  310. package/bundled/skills-windows/refactor-planner/SKILL.md +0 -398
  311. package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +0 -292
  312. package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +0 -301
  313. package/bundled/skills-windows/refactor-planner/references/fast-path.md +0 -59
  314. package/bundled/skills-windows/refactor-planner/references/planning-phases.md +0 -135
  315. package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +0 -221
  316. package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +0 -858
  317. package/bundled/skills-windows/refactor-workflow/SKILL.md +0 -343
  318. package/bundled/skills-windows/refactor-workflow/references/brainstorm-guide.md +0 -116
  319. package/bundled/templates/hooks/diff-prizm-docs.sh +0 -158
  320. package/bundled/templates/hooks/prizm-pre-commit.sh +0 -108
  321. package/bundled/templates/hooks/validate-prizm-docs.sh +0 -139
@@ -1,2282 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Generate a session-specific bootstrap prompt from template and feature list.
3
-
4
- Supports two modes:
5
- 1. **Section assembly** (preferred): Loads modular section files from
6
- templates/sections/ and assembles them based on tier, conditions, and
7
- feature configuration. Conditional logic is handled in Python code,
8
- not regex-based template blocks.
9
- 2. **Legacy template** (fallback): Reads a monolithic bootstrap-tier{1,2,3}.md
10
- template and resolves {{PLACEHOLDER}} variables and {{IF_xxx}} blocks.
11
-
12
- The section assembly mode is used when templates/sections/ directory exists.
13
- Otherwise, falls back to legacy templates for backward compatibility.
14
-
15
- Usage:
16
- python3 generate-bootstrap-prompt.py \
17
- --feature-list <path> --feature-id <id> \
18
- --session-id <id> --run-id <id> \
19
- --retry-count <n> --resume-phase <n|null> \
20
- --output <path>
21
- """
22
-
23
- import argparse
24
- import glob
25
- import json
26
- import os
27
- import re
28
- import sys
29
-
30
- from utils import enrich_global_context, load_json_file, read_platform_conventions, setup_logging
31
-
32
-
33
- DEFAULT_MAX_RETRIES = 3
34
- DEFAULT_MAX_LOG_SIZE = 2097152
35
- DEFAULT_LOG_MONITOR_POLL_INTERVAL = 30
36
-
37
- LOGGER = setup_logging("generate-bootstrap-prompt")
38
-
39
-
40
- def parse_args():
41
- parser = argparse.ArgumentParser(
42
- description=(
43
- "Generate a session-specific bootstrap prompt from a template "
44
- "and .prizmkit/plans/feature-list.json."
45
- )
46
- )
47
- parser.add_argument(
48
- "--feature-list",
49
- required=True,
50
- help="Path to .prizmkit/plans/feature-list.json",
51
- )
52
- parser.add_argument(
53
- "--feature-id",
54
- required=True,
55
- help="Feature ID to generate prompt for (e.g. F-001)",
56
- )
57
- parser.add_argument(
58
- "--session-id",
59
- required=True,
60
- help="Session ID for this pipeline session",
61
- )
62
- parser.add_argument(
63
- "--run-id",
64
- required=True,
65
- help="Pipeline run ID",
66
- )
67
- parser.add_argument(
68
- "--retry-count",
69
- required=True,
70
- help="Current retry count",
71
- )
72
- parser.add_argument(
73
- "--resume-phase",
74
- required=True,
75
- help='Phase to resume from, or "null" for fresh start',
76
- )
77
- parser.add_argument(
78
- "--state-dir",
79
- default=None,
80
- help="State directory path for reading previous session info",
81
- )
82
- parser.add_argument(
83
- "--output",
84
- required=True,
85
- help="Output path for the rendered prompt",
86
- )
87
- parser.add_argument(
88
- "--template",
89
- default=None,
90
- help=(
91
- "Custom template path. Defaults to "
92
- "{script_dir}/../templates/bootstrap-prompt.md"
93
- ),
94
- )
95
- parser.add_argument(
96
- "--mode",
97
- choices=["lite", "standard", "full"],
98
- default=None,
99
- help="Override pipeline mode (default: auto-detect from complexity)",
100
- )
101
- parser.add_argument(
102
- "--critic",
103
- choices=["true", "false"],
104
- default=None,
105
- help="Override critic enablement (default: read from feature field)",
106
- )
107
- parser.add_argument(
108
- "--extract-baselines",
109
- action="store_true",
110
- help="Run tests and extract baseline failures (slower, optional)",
111
- )
112
- parser.add_argument(
113
- "--no-checkpoint",
114
- action="store_true",
115
- help="Do not write workflow-checkpoint.json (used by pipeline dry-run)",
116
- )
117
-
118
- return parser.parse_args()
119
-
120
-
121
- def read_text_file(path):
122
- """Read and return the text content of a file."""
123
- abs_path = os.path.abspath(path)
124
- if not os.path.isfile(abs_path):
125
- return None, "File not found: {}".format(abs_path)
126
- try:
127
- with open(abs_path, "r", encoding="utf-8") as f:
128
- return f.read(), None
129
- except IOError as e:
130
- return None, "Cannot read file: {}".format(str(e))
131
-
132
-
133
- def find_feature(features, feature_id):
134
- """Find and return the feature dict matching the given ID."""
135
- for feature in features:
136
- if isinstance(feature, dict) and feature.get("id") == feature_id:
137
- return feature
138
- return None
139
-
140
-
141
- def compute_feature_slug(feature_id, title):
142
- """Compute the prizmkit feature slug: ###-kebab-case-name.
143
-
144
- e.g. F-001 + "Project Infrastructure Setup" -> "001-project-infrastructure-setup"
145
- The prizmkit skills use this slug to create per-feature directories.
146
- """
147
- # Extract numeric part from feature_id (e.g., "F-001" -> "001")
148
- numeric = feature_id.replace("F-", "").replace("f-", "")
149
- # Pad to 3 digits
150
- numeric = numeric.zfill(3)
151
-
152
- # Convert title to kebab-case
153
- slug = title.lower()
154
- slug = re.sub(r"[^a-z0-9\s-]", "", slug) # remove non-alphanumeric
155
- slug = re.sub(r"[\s]+", "-", slug.strip()) # spaces to hyphens
156
- slug = re.sub(r"-+", "-", slug) # collapse multiple hyphens
157
- slug = slug.strip("-")
158
-
159
- return "{}-{}".format(numeric, slug)
160
-
161
-
162
- def _format_bytes(n):
163
- """Format a byte count to a human-readable string."""
164
- if n >= 1048576:
165
- return "{}MB".format(n // 1048576)
166
- elif n >= 1024:
167
- return "{}KB".format(n // 1024)
168
- return "{}B".format(n)
169
-
170
-
171
- def _parse_non_negative_int_env(name, default):
172
- """Parse a non-negative integer environment variable."""
173
- value = os.environ.get(name, str(default))
174
- try:
175
- parsed = int(value)
176
- except ValueError:
177
- emit_failure("{} must be a non-negative integer".format(name))
178
- if parsed < 0:
179
- emit_failure("{} must be a non-negative integer".format(name))
180
- return parsed
181
-
182
-
183
- def _parse_positive_int_env(name, default):
184
- """Parse a positive integer environment variable."""
185
- value = os.environ.get(name, str(default))
186
- try:
187
- parsed = int(value)
188
- except ValueError:
189
- emit_failure("{} must be a positive integer".format(name))
190
- if parsed <= 0:
191
- emit_failure("{} must be a positive integer".format(name))
192
- return parsed
193
-
194
-
195
-
196
- def load_log_size_section(script_dir):
197
- """Load the optional log-size awareness prompt section."""
198
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
199
- if max_log_size == 0:
200
- return ""
201
-
202
- section_path = os.path.join(
203
- script_dir, "..", "templates", "sections", "log-size-awareness.md",
204
- )
205
- content, err = read_text_file(section_path)
206
- if err:
207
- return ""
208
- return content
209
-
210
- def format_acceptance_criteria(criteria):
211
- """Format acceptance criteria as a markdown bullet list."""
212
- if not criteria:
213
- return "- (none specified)"
214
- lines = []
215
- for item in criteria:
216
- lines.append("- {}".format(item))
217
- return "\n".join(lines)
218
-
219
- def dedupe_test_commands(test_commands):
220
- """Return test commands with original order preserved."""
221
- return list(dict.fromkeys(test_commands))
222
-
223
-
224
- def format_powershell_test_commands(test_commands):
225
- """Format test commands as PowerShell statements with fail-fast guards."""
226
- commands = dedupe_test_commands(test_commands)
227
- if not commands:
228
- return ""
229
- guarded_commands = [
230
- "{}; if ($LASTEXITCODE -ne 0) {{ exit $LASTEXITCODE }}".format(command)
231
- for command in commands
232
- ]
233
- return "; ".join(guarded_commands)
234
-
235
-
236
- def format_baseline_test_commands(test_commands):
237
- """Format test commands for Python's sequential baseline extraction."""
238
- return dedupe_test_commands(test_commands)
239
-
240
-
241
- def detect_test_command_list(project_root):
242
- """
243
- Auto-detect test commands based on project structure.
244
-
245
- Returns: ordered list of test commands.
246
- """
247
- test_commands = []
248
-
249
- # Check for npm/package.json
250
- if os.path.exists(os.path.join(project_root, "package.json")):
251
- test_commands.append("npm test")
252
-
253
- # Check for Go
254
- if os.path.exists(os.path.join(project_root, "go.mod")):
255
- test_commands.append("go test ./...")
256
-
257
- # Check for Rust/Cargo
258
- if os.path.exists(os.path.join(project_root, "Cargo.toml")):
259
- test_commands.append("cargo test")
260
-
261
- # Check for Python pytest
262
- if os.path.exists(os.path.join(project_root, "pytest.ini")) or \
263
- os.path.exists(os.path.join(project_root, "setup.py")):
264
- test_commands.append("pytest")
265
-
266
- # Check for Make test target
267
- makefile_path = os.path.join(project_root, "Makefile")
268
- if os.path.exists(makefile_path):
269
- try:
270
- with open(makefile_path, 'r') as f:
271
- if 'test:' in f.read():
272
- test_commands.append("make test")
273
- except Exception:
274
- pass
275
-
276
- return dedupe_test_commands(test_commands)
277
-
278
-
279
- def detect_test_commands(project_root):
280
- """Auto-detect and format test commands for Windows PowerShell prompts."""
281
- return format_powershell_test_commands(detect_test_command_list(project_root))
282
-
283
-
284
- def extract_baseline_failures(test_commands, project_root):
285
- """
286
- Run test command and extract failing tests.
287
-
288
- Returns: semicolon-separated list of failing test names
289
- """
290
- if isinstance(test_commands, str):
291
- test_commands = [test_commands]
292
- test_commands = [command for command in test_commands if command]
293
-
294
- if not test_commands or test_commands[0].startswith("(auto-detection"):
295
- return ""
296
-
297
- original_cwd = os.getcwd()
298
- try:
299
- import subprocess
300
- os.chdir(project_root)
301
-
302
- output_parts = []
303
- for command in test_commands:
304
- result = subprocess.run(
305
- command,
306
- shell=True,
307
- capture_output=True,
308
- text=True,
309
- timeout=120
310
- )
311
- output_parts.append(result.stdout + result.stderr)
312
-
313
- os.chdir(original_cwd)
314
-
315
- output = "\n".join(output_parts)
316
- failures = []
317
-
318
- for line in output.split('\n'):
319
- if 'FAILED' in line and '::' in line:
320
- parts = line.split('FAILED')
321
- if len(parts) > 1:
322
- test_name = parts[1].strip().split(' ')[0]
323
- if test_name and test_name not in failures:
324
- failures.append(test_name)
325
-
326
- return ";".join(failures) if failures else ""
327
-
328
- except Exception as e:
329
- return f"(error: {str(e)})"
330
- finally:
331
- try:
332
- os.chdir(original_cwd)
333
- except Exception:
334
- pass
335
-
336
-
337
- def format_ac_checklist(acceptance_criteria):
338
- """Format acceptance criteria as a markdown checkbox list."""
339
- if not acceptance_criteria:
340
- return "- (no Verification Gates specified)"
341
- lines = []
342
- for item in acceptance_criteria:
343
- lines.append("- [ ] {}".format(item))
344
- return "\n".join(lines)
345
-
346
-
347
-
348
- def format_global_context(global_context, project_root=None):
349
- """Format global_context dict as a key-value list.
350
-
351
- If global_context is empty/sparse and project_root is provided,
352
- auto-detect tech stack from project files to fill gaps.
353
- """
354
- if project_root:
355
- enrich_global_context(global_context, project_root)
356
-
357
- if not global_context:
358
- return "- (none specified)"
359
- lines = []
360
- for key, value in sorted(global_context.items()):
361
- lines.append("- **{}**: {}".format(key, value))
362
- return "\n".join(lines)
363
-
364
-
365
- def format_user_context(user_context):
366
- """Format user_context array as a markdown section.
367
-
368
- Returns empty string if user_context is empty or absent,
369
- so the template placeholder resolves to nothing.
370
- """
371
- if not user_context or not isinstance(user_context, list):
372
- return ""
373
- items = [item for item in user_context if isinstance(item, str) and item.strip()]
374
- if not items:
375
- return ""
376
- lines = [
377
- "> These materials were provided by the user and are authoritative "
378
- "when they clarify or constrain this feature. They do not expand "
379
- "the current scope by themselves; use the Task Contract to decide "
380
- "what belongs to this session.",
381
- "",
382
- ]
383
- for item in items:
384
- lines.append("- {}".format(item))
385
- return "\n".join(lines)
386
-
387
-
388
- def get_completed_dependencies(features, feature):
389
- """Look up dependency features and list those with status=completed.
390
-
391
- When a completed dependency has completion_notes (written by the AI
392
- session and propagated by the pipeline runner), include them as rich
393
- context so the downstream session knows what was built.
394
- """
395
- deps = feature.get("dependencies", [])
396
- if not deps:
397
- return "- (no dependencies)"
398
-
399
- # Build a lookup map
400
- feature_map = {}
401
- for f in features:
402
- if isinstance(f, dict) and "id" in f:
403
- feature_map[f["id"]] = f
404
-
405
- sections = []
406
- for dep_id in deps:
407
- dep = feature_map.get(dep_id)
408
- if dep and dep.get("status") == "completed":
409
- header = "- **{}** — {} (completed)".format(
410
- dep_id, dep.get("title", "Untitled")
411
- )
412
- notes = dep.get("completion_notes", [])
413
- if notes and isinstance(notes, list):
414
- note_lines = "\n".join(
415
- " - {}".format(n) for n in notes
416
- if isinstance(n, str) and n.strip()
417
- )
418
- if note_lines:
419
- header += "\n" + note_lines
420
- sections.append(header)
421
-
422
- if not sections:
423
- return "- (no completed dependencies yet)"
424
- return "\n".join(sections)
425
-
426
-
427
- def get_prev_session_status(state_dir, feature_id):
428
- """Read previous session status from state dir if available."""
429
- if not state_dir:
430
- return "N/A (first run)"
431
-
432
- # Try to read the feature status file to find the last session
433
- feature_status_path = os.path.join(state_dir, feature_id, "status.json")
434
- if not os.path.isfile(feature_status_path):
435
- return "N/A (first run)"
436
-
437
- try:
438
- with open(feature_status_path, "r", encoding="utf-8") as f:
439
- feature_status = json.load(f)
440
- except (json.JSONDecodeError, IOError):
441
- return "N/A (could not read feature status)"
442
-
443
- last_session_id = feature_status.get("last_session_id")
444
- if not last_session_id:
445
- return "N/A (first run)"
446
-
447
- # Try to read the last session's session-status.json
448
- session_status_path = os.path.join(state_dir, feature_id, "sessions",
449
- last_session_id, "session-status.json"
450
- )
451
- if not os.path.isfile(session_status_path):
452
- return "N/A (previous session status file not found)"
453
-
454
- try:
455
- with open(session_status_path, "r", encoding="utf-8") as f:
456
- session_data = json.load(f)
457
- except (json.JSONDecodeError, IOError):
458
- return "N/A (could not read previous session status)"
459
-
460
- status = session_data.get("status", "unknown")
461
- checkpoint = session_data.get("checkpoint_reached", "none")
462
- current_phase = session_data.get("current_phase", "unknown")
463
- errors = session_data.get("errors", [])
464
-
465
- result = "{} (checkpoint: {}, last phase: {})".format(
466
- status, checkpoint, current_phase
467
- )
468
- if errors:
469
- result += " — errors: {}".format("; ".join(str(e) for e in errors))
470
- return result
471
-
472
-
473
- def _read_project_brief(project_root):
474
- """Read project-brief.md from new or old location with fallback.
475
-
476
- Returns the file content as a string, or a fallback message if absent.
477
- This brief is generated by app-planner during interactive planning and
478
- captures the user's product ideas as a checklist. Each line is one idea,
479
- marked [ ] for pending or [x] for completed. Feature sessions should mark
480
- items [x] and append key file paths when implementing relevant ideas.
481
- """
482
- # Check both new and old paths for backward compatibility
483
- new_path = os.path.join(project_root, ".prizmkit", "plans", "project-brief.md")
484
- old_path = os.path.join(project_root, "project-brief.md")
485
-
486
- for brief_path in [new_path, old_path]:
487
- if os.path.isfile(brief_path):
488
- try:
489
- with open(brief_path, "r", encoding="utf-8") as f:
490
- content = f.read().strip()
491
- if brief_path == old_path:
492
- # Warn user about old path
493
- import sys
494
- print("⚠️ Migration notice: project-brief.md found in root. "
495
- "Please move to .prizmkit/plans/project-brief.md",
496
- file=sys.stderr)
497
- return content
498
- except IOError:
499
- return "(project-brief.md exists but could not be read)"
500
-
501
- return "(No project brief available)"
502
-
503
-
504
- def resolve_project_root(script_dir):
505
- """Resolve project root from script_dir of the prompt-generator script.
506
-
507
- Layout-aware:
508
- <project>/.prizmkit/dev-pipeline/scripts/ → project root = <project>
509
- <repo>/dev-pipeline/scripts/ → project root = <repo>
510
- """
511
- pipeline_dir = os.path.dirname(script_dir)
512
- pipeline_parent = os.path.dirname(pipeline_dir)
513
- if os.path.basename(pipeline_parent) == ".prizmkit":
514
- return os.path.abspath(os.path.dirname(pipeline_parent))
515
- return os.path.abspath(pipeline_parent)
516
-
517
-
518
- def process_conditional_blocks(content, resume_phase):
519
- """Handle conditional blocks based on resume_phase.
520
-
521
- Supports:
522
- - {{IF_FRESH_START}} / {{END_IF_FRESH_START}}
523
- - {{IF_RESUME}} / {{END_IF_RESUME}}
524
- - {{IF_RETRY}} / {{END_IF_RETRY}}
525
- """
526
- is_resume = resume_phase != "null"
527
-
528
- if is_resume:
529
- # Remove fresh-start blocks, keep resume blocks
530
- content = re.sub(
531
- r"\{\{IF_FRESH_START\}\}.*?\{\{END_IF_FRESH_START\}\}\n?",
532
- "", content, flags=re.DOTALL,
533
- )
534
- content = re.sub(r"\{\{IF_RESUME\}\}\n?", "", content)
535
- content = re.sub(r"\{\{END_IF_RESUME\}\}\n?", "", content)
536
- else:
537
- # Keep fresh-start blocks, remove resume blocks
538
- content = re.sub(r"\{\{IF_FRESH_START\}\}\n?", "", content)
539
- content = re.sub(r"\{\{END_IF_FRESH_START\}\}\n?", "", content)
540
- content = re.sub(
541
- r"\{\{IF_RESUME\}\}.*?\{\{END_IF_RESUME\}\}\n?",
542
- "", content, flags=re.DOTALL,
543
- )
544
-
545
- return content
546
-
547
-
548
- def process_mode_blocks(content, pipeline_mode, init_done, critic_enabled=False,
549
- browser_interaction=False, browser_tool="auto"):
550
- """Process pipeline mode, init, critic, and browser conditional blocks.
551
-
552
- Keeps the block matching the current mode, removes the others.
553
- Handles {{IF_CRITIC_ENABLED}} / {{END_IF_CRITIC_ENABLED}} blocks.
554
- Handles {{IF_BROWSER_INTERACTION}} / {{END_IF_BROWSER_INTERACTION}} blocks.
555
- Handles {{IF_BROWSER_TOOL_PLAYWRIGHT}} / {{IF_BROWSER_TOOL_OPENCLI}} /
556
- {{IF_BROWSER_TOOL_AUTO}} blocks (nested inside browser interaction block).
557
- """
558
- # Handle lite/standard/full blocks
559
- modes = ["lite", "standard", "full"]
560
-
561
- for mode in modes:
562
- tag_open = "{{{{IF_MODE_{}}}}}".format(mode.upper())
563
- tag_close = "{{{{END_IF_MODE_{}}}}}".format(mode.upper())
564
-
565
- if mode == pipeline_mode:
566
- # Keep content, remove tags
567
- content = content.replace(tag_open + "\n", "")
568
- content = content.replace(tag_open, "")
569
- content = content.replace(tag_close + "\n", "")
570
- content = content.replace(tag_close, "")
571
- else:
572
- # Remove entire block
573
- pattern = re.escape(tag_open) + r".*?" + re.escape(tag_close) + r"\n?"
574
- content = re.sub(pattern, "", content, flags=re.DOTALL)
575
-
576
- # Init blocks
577
- if init_done:
578
- content = re.sub(r"\{\{IF_INIT_DONE\}\}\n?", "", content)
579
- content = re.sub(r"\{\{END_IF_INIT_DONE\}\}\n?", "", content)
580
- content = re.sub(
581
- r"\{\{IF_INIT_NEEDED\}\}.*?\{\{END_IF_INIT_NEEDED\}\}\n?",
582
- "", content, flags=re.DOTALL,
583
- )
584
- else:
585
- content = re.sub(r"\{\{IF_INIT_NEEDED\}\}\n?", "", content)
586
- content = re.sub(r"\{\{END_IF_INIT_NEEDED\}\}\n?", "", content)
587
- content = re.sub(
588
- r"\{\{IF_INIT_DONE\}\}.*?\{\{END_IF_INIT_DONE\}\}\n?",
589
- "", content, flags=re.DOTALL,
590
- )
591
-
592
- # Critic blocks
593
- critic_open = "{{IF_CRITIC_ENABLED}}"
594
- critic_close = "{{END_IF_CRITIC_ENABLED}}"
595
- if critic_enabled:
596
- # Keep content, remove tags
597
- content = content.replace(critic_open + "\n", "")
598
- content = content.replace(critic_open, "")
599
- content = content.replace(critic_close + "\n", "")
600
- content = content.replace(critic_close, "")
601
- else:
602
- # Remove entire CRITIC blocks
603
- pattern = re.escape(critic_open) + r".*?" + re.escape(critic_close) + r"\n?"
604
- content = re.sub(pattern, "", content, flags=re.DOTALL)
605
-
606
- # Browser interaction blocks
607
- browser_open = "{{IF_BROWSER_INTERACTION}}"
608
- browser_close = "{{END_IF_BROWSER_INTERACTION}}"
609
- if browser_interaction:
610
- content = content.replace(browser_open + "\n", "")
611
- content = content.replace(browser_open, "")
612
- content = content.replace(browser_close + "\n", "")
613
- content = content.replace(browser_close, "")
614
- else:
615
- pattern = re.escape(browser_open) + r".*?" + re.escape(browser_close) + r"\n?"
616
- content = re.sub(pattern, "", content, flags=re.DOTALL)
617
-
618
- # Browser tool selection blocks (nested inside browser interaction)
619
- tool_variants = ["PLAYWRIGHT", "OPENCLI", "AUTO"]
620
- # Map browser_tool value to the variant tag name
621
- active_variant = {
622
- "playwright-cli": "PLAYWRIGHT",
623
- "opencli": "OPENCLI",
624
- "auto": "AUTO",
625
- }.get(browser_tool, "AUTO")
626
-
627
- for variant in tool_variants:
628
- tool_open = "{{{{IF_BROWSER_TOOL_{}}}}}".format(variant)
629
- tool_close = "{{{{END_IF_BROWSER_TOOL_{}}}}}".format(variant)
630
- if variant == active_variant and browser_interaction:
631
- # Keep content, remove tags
632
- content = content.replace(tool_open + "\n", "")
633
- content = content.replace(tool_open, "")
634
- content = content.replace(tool_close + "\n", "")
635
- content = content.replace(tool_close, "")
636
- else:
637
- # Remove entire block
638
- pat = re.escape(tool_open) + r".*?" + re.escape(tool_close) + r"\n?"
639
- content = re.sub(pat, "", content, flags=re.DOTALL)
640
-
641
- return content
642
-
643
-
644
- def detect_init_status(project_root):
645
- """Check if PrizmKit init has already been done."""
646
- prizm_docs = os.path.join(project_root, ".prizmkit/prizm-docs", "root.prizm")
647
- prizmkit_config = os.path.join(project_root, ".prizmkit", "config.json")
648
- return os.path.isfile(prizm_docs) and os.path.isfile(prizmkit_config)
649
-
650
-
651
- def detect_existing_artifacts(project_root, feature_slug):
652
- """Check which planning artifacts already exist for this feature.
653
-
654
- Returns a dict with keys: has_spec, has_plan, all_complete.
655
- Tasks are now part of plan.md (Tasks section), not a separate file.
656
- """
657
- specs_dir = os.path.join(project_root, ".prizmkit", "specs", feature_slug)
658
- result = {
659
- "has_spec": os.path.isfile(os.path.join(specs_dir, "spec.md")),
660
- "has_plan": os.path.isfile(os.path.join(specs_dir, "plan.md")),
661
- }
662
- result["all_complete"] = all([
663
- result["has_spec"], result["has_plan"]
664
- ])
665
- return result
666
-
667
-
668
- def determine_pipeline_mode(complexity):
669
- """Map estimated_complexity to pipeline mode.
670
-
671
- Returns: 'lite', 'standard', or 'full'
672
-
673
- Tier assignment rationale:
674
- - low + medium → lite (single agent): most features don't benefit from
675
- the orchestrator→dev→reviewer overhead. A single agent reading
676
- .prizmkit/prizm-docs + implementing directly is faster and cheaper.
677
- - high → standard (orchestrator + dev + reviewer): complex features
678
- need the spec→plan→analyze→implement→review pipeline.
679
- - critical → full (full team + framework guardrails): architectural
680
- changes that touch many files and need extra safety checks.
681
- """
682
- mapping = {
683
- "low": "lite",
684
- "medium": "lite",
685
- "high": "standard",
686
- "critical": "full",
687
- }
688
- return mapping.get(complexity, "lite")
689
-
690
-
691
- # ============================================================
692
- # Checkpoint generation
693
- # ============================================================
694
-
695
- # Mapping: section name -> (skill_key, display_name, required_artifacts)
696
- # skill_key is a unique identifier in the checkpoint, not necessarily the
697
- # prizmkit skill name. This ensures each section has a distinct key so
698
- # merge_checkpoint_state() never collides.
699
- SECTION_TO_SKILL = {
700
- "phase0-init": ("prizmkit-init", "Project Bootstrap",
701
- [".prizmkit/prizm-docs/root.prizm", ".prizmkit/config.json"]),
702
- "phase0-test-baseline": ("test-baseline", "Test Baseline", []),
703
- "phase-context-snapshot": ("context-snapshot", "Build Context Snapshot",
704
- [".prizmkit/specs/{slug}/context-snapshot.md"]),
705
- "phase-specify-plan": ("context-snapshot-and-plan", "Specify & Plan",
706
- [".prizmkit/specs/{slug}/context-snapshot.md",
707
- ".prizmkit/specs/{slug}/plan.md"]),
708
- "phase-plan": ("prizmkit-plan", "Plan & Tasks",
709
- [".prizmkit/specs/{slug}/plan.md"]),
710
- "phase-critic-plan": ("critic-plan-review", "Critic: Plan Review", []),
711
- "phase-implement": ("prizmkit-implement", "Implement + Test", []),
712
- "phase-prizmkit-test": (
713
- "prizmkit-test", "Scoped Feature Test Gate",
714
- [".prizmkit/specs/{slug}/test-report-path.txt",
715
- ".prizmkit/test/*/test-report.md"],
716
- ),
717
- "phase-review": ("prizmkit-code-review", "Code Review", []),
718
- "phase-browser": ("browser-verification", "Browser Verification", []),
719
- "phase-commit": None, # special: split into retrospective + committer
720
- }
721
-
722
- # phase-commit is split into two steps
723
- _COMMIT_STEPS = [
724
- ("prizmkit-retrospective", "Retrospective", []),
725
- ("prizmkit-committer", "Commit", ["--headless"]),
726
- ]
727
-
728
-
729
- def _resolve_artifacts(artifact_templates, slug):
730
- """Replace {slug} placeholder with the actual feature slug."""
731
- return [a.replace("{slug}", slug) for a in artifact_templates]
732
-
733
-
734
- def _artifact_exists(project_root, artifact):
735
- """Return whether an artifact path or glob exists under project_root."""
736
- full_path = os.path.join(project_root, artifact)
737
- if glob.has_magic(full_path):
738
- return bool(glob.glob(full_path))
739
- return os.path.exists(full_path)
740
-
741
-
742
- def _read_text(path):
743
- """Read a UTF-8 text file, returning an empty string on IO errors."""
744
- try:
745
- with open(path, "r", encoding="utf-8") as f:
746
- return f.read()
747
- except IOError:
748
- return ""
749
-
750
-
751
- def _normalize_artifact_dir(path):
752
- """Normalize report artifact_dir values for comparison."""
753
- return path.strip().replace("\\", "/").rstrip("/")
754
-
755
-
756
- def _report_path_matches_artifacts(project_root, report_path, artifacts):
757
- """Return whether report_path is an in-project test report artifact."""
758
- test_root = os.path.realpath(
759
- os.path.join(project_root, ".prizmkit", "test")
760
- )
761
- resolved_report = os.path.realpath(report_path)
762
-
763
- try:
764
- if os.path.commonpath([test_root, resolved_report]) != test_root:
765
- return False
766
- except ValueError:
767
- return False
768
-
769
- report_artifacts = [a for a in artifacts if a.endswith("test-report.md")]
770
- for artifact in report_artifacts:
771
- artifact_path = os.path.join(project_root, artifact)
772
- if glob.has_magic(artifact_path):
773
- candidates = glob.glob(artifact_path)
774
- else:
775
- candidates = [artifact_path]
776
- for candidate in candidates:
777
- if os.path.realpath(candidate) == resolved_report:
778
- return True
779
- return False
780
-
781
-
782
- def _extract_scope_field(report_text, field_name):
783
- """Extract a '- Field: value' entry from the report Scope section."""
784
- in_scope = False
785
- prefix = "- {}:".format(field_name)
786
- for raw_line in report_text.splitlines():
787
- line = raw_line.strip()
788
- if line == "## Scope":
789
- in_scope = True
790
- continue
791
- if in_scope and line.startswith("## "):
792
- break
793
- if in_scope and line.startswith(prefix):
794
- return line[len(prefix):].strip()
795
- return ""
796
-
797
-
798
- def _extract_report_verdict(report_text):
799
- """Extract the first non-empty line after the report Verdict heading."""
800
- in_verdict = False
801
- for raw_line in report_text.splitlines():
802
- line = raw_line.strip()
803
- if line == "## Verdict":
804
- in_verdict = True
805
- continue
806
- if in_verdict:
807
- if line.startswith("## "):
808
- break
809
- if line:
810
- return line
811
- return ""
812
-
813
-
814
- def _extract_report_section(report_text, heading):
815
- """Extract text under a level-2 markdown heading."""
816
- in_section = False
817
- lines = []
818
- for raw_line in report_text.splitlines():
819
- line = raw_line.strip()
820
- if line == heading:
821
- in_section = True
822
- continue
823
- if in_section and line.startswith("## "):
824
- break
825
- if in_section:
826
- lines.append(raw_line)
827
- return "\n".join(lines)
828
-
829
-
830
- def _discover_openapi_file(project_root):
831
- """Return the first common OpenAPI/Swagger file under project_root."""
832
- candidates = [
833
- "openapi.yaml", "openapi.yml", "swagger.yaml", "swagger.yml",
834
- "docs/openapi.yaml", "docs/openapi.yml",
835
- "docs/swagger.yaml", "docs/swagger.yml",
836
- "api/openapi.yaml", "api/openapi.yml",
837
- "api/swagger.yaml", "api/swagger.yml",
838
- ]
839
- for candidate in candidates:
840
- full_path = os.path.join(project_root, candidate)
841
- if os.path.isfile(full_path):
842
- return full_path
843
- return ""
844
-
845
-
846
- def _normalize_api_path(path):
847
- """Normalize API path tokens for exact comparison."""
848
- normalized = path.strip().strip("'\"`")
849
- if len(normalized) > 1:
850
- normalized = normalized.rstrip("/")
851
- return normalized
852
-
853
-
854
- def _extract_openapi_operations(openapi_text):
855
- """Extract OpenAPI method+path operations without requiring PyYAML."""
856
- methods = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE"}
857
- operations = []
858
- in_paths = False
859
- paths_indent = 0
860
- current_path = ""
861
- current_path_indent = 0
862
- path_key_re = re.compile(
863
- r"^\s*(?:(['\"])(/.*?)\1|(/[^:\s]+))\s*:\s*(?:#.*)?$"
864
- )
865
- method_re = re.compile(r"^\s*([A-Za-z]+)\s*:\s*(?:#.*)?$")
866
-
867
- for raw_line in openapi_text.splitlines():
868
- stripped = raw_line.strip()
869
- if not stripped or stripped.startswith("#"):
870
- continue
871
-
872
- indent = len(raw_line) - len(raw_line.lstrip())
873
- if not in_paths:
874
- if re.match(r"^\s*paths\s*:\s*(?:#.*)?$", raw_line):
875
- in_paths = True
876
- paths_indent = indent
877
- continue
878
-
879
- if indent <= paths_indent:
880
- break
881
-
882
- path_match = path_key_re.match(raw_line)
883
- if path_match:
884
- current_path = path_match.group(2) or path_match.group(3) or ""
885
- current_path_indent = indent
886
- continue
887
-
888
- method_match = method_re.match(raw_line)
889
- if current_path and method_match and indent > current_path_indent:
890
- method = method_match.group(1).upper()
891
- if method in methods:
892
- operations.append((method, _normalize_api_path(current_path)))
893
-
894
- return operations
895
-
896
-
897
- def _extract_matrix_operations(rows):
898
- """Extract exact method+path tokens from Boundary Matrix rows."""
899
- operations = set()
900
- path_only = set()
901
- token_re = re.compile(
902
- r"(?:(\b(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b)\s+)?(/[^\s,`|)]+)",
903
- flags=re.IGNORECASE,
904
- )
905
- for row in rows:
906
- interface = row.get("Interface", "")
907
- for match in token_re.finditer(interface):
908
- method = match.group(1).upper() if match.group(1) else ""
909
- path = _normalize_api_path(match.group(2))
910
- if not path:
911
- continue
912
- if method:
913
- operations.add((method, path))
914
- else:
915
- path_only.add(path)
916
- return operations, path_only
917
-
918
-
919
- def _boundary_report_sections_valid(project_root, report_text):
920
- """Return whether required boundary sections do not report failure."""
921
- required_columns = [
922
- "Module",
923
- "Interface",
924
- "Service Type",
925
- "Happy Path",
926
- "Request Validation",
927
- "Auth / Permission / Ownership",
928
- "Domain Invariants",
929
- "Collection / Pagination",
930
- "Date / Time",
931
- "State Transitions",
932
- "Dependency Failure",
933
- "Response Contract",
934
- "N/A Reasons",
935
- "Final Status",
936
- ]
937
- category_columns = [
938
- "Happy Path",
939
- "Request Validation",
940
- "Auth / Permission / Ownership",
941
- "Domain Invariants",
942
- "Collection / Pagination",
943
- "Date / Time",
944
- "State Transitions",
945
- "Dependency Failure",
946
- "Response Contract",
947
- ]
948
- allowed_final_statuses = {
949
- "boundary-covered",
950
- "generated-needs-review",
951
- "skipped-with-reason",
952
- }
953
- incomplete_markers = {
954
- "boundary-missing",
955
- "happy-path-only",
956
- "unresolved",
957
- }
958
-
959
- matrix_section = _extract_report_section(report_text, "## Boundary Matrix")
960
- if not matrix_section:
961
- return False
962
- matrix_lines = [line.strip() for line in matrix_section.splitlines()
963
- if line.strip().startswith("|")]
964
- headers = []
965
- rows = []
966
- for line in matrix_lines:
967
- cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
968
- if not headers:
969
- if "Interface" in cells and "Final Status" in cells:
970
- headers = cells
971
- continue
972
- if all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells):
973
- continue
974
- if len(cells) < len(headers):
975
- cells.extend([""] * (len(headers) - len(cells)))
976
- row = {header: cells[i] if i < len(cells) else ""
977
- for i, header in enumerate(headers)}
978
- if row.get("Interface", "").strip(".` ") in {"", "..."}:
979
- continue
980
- rows.append(row)
981
-
982
- if not headers or not rows:
983
- return False
984
- if any(column not in headers for column in required_columns):
985
- return False
986
-
987
- for row in rows:
988
- final_status = row.get("Final Status", "").strip().lower()
989
- if final_status not in allowed_final_statuses:
990
- return False
991
- for column in category_columns:
992
- if not row.get(column, "").strip():
993
- return False
994
- for value in row.values():
995
- normalized = value.strip().lower()
996
- if any(marker in normalized for marker in incomplete_markers):
997
- return False
998
-
999
- openapi_file = _discover_openapi_file(project_root)
1000
- if openapi_file:
1001
- operations = _extract_openapi_operations(_read_text(openapi_file))
1002
- matrix_operations, _matrix_path_only = _extract_matrix_operations(rows)
1003
- for method, path in operations:
1004
- if (method, path) not in matrix_operations:
1005
- return False
1006
-
1007
- gate_section = _extract_report_section(report_text,
1008
- "## Boundary Completion Gate")
1009
- if not gate_section:
1010
- return False
1011
- completion_gate_passed = False
1012
- boundary_missing_seen = False
1013
- happy_path_only_seen = False
1014
- for raw_line in gate_section.splitlines():
1015
- line = raw_line.strip()
1016
- match = re.match(r"-?\s*(Boundary-missing|Happy-path-only):\s*(\d+)\s*$",
1017
- line, re.IGNORECASE)
1018
- if match:
1019
- if match.group(1).lower() == "boundary-missing":
1020
- boundary_missing_seen = True
1021
- if match.group(1).lower() == "happy-path-only":
1022
- happy_path_only_seen = True
1023
- if int(match.group(2)) > 0:
1024
- return False
1025
- if re.match(r"-?\s*Completion gate passed:\s*yes\s*$",
1026
- line, re.IGNORECASE):
1027
- completion_gate_passed = True
1028
- if re.match(r"-?\s*Completion gate passed:\s*no\b",
1029
- line, re.IGNORECASE):
1030
- return False
1031
- if not (completion_gate_passed and boundary_missing_seen and
1032
- happy_path_only_seen):
1033
- return False
1034
-
1035
- validation_section = _extract_report_section(report_text,
1036
- "## Boundary Validation")
1037
- if not validation_section:
1038
- return False
1039
- validator_result = ""
1040
- for raw_line in validation_section.splitlines():
1041
- line = raw_line.strip()
1042
- match = re.match(r"-?\s*Validator result:\s*(\S+)",
1043
- line, re.IGNORECASE)
1044
- if match:
1045
- validator_result = match.group(1).strip().lower()
1046
- break
1047
-
1048
- return validator_result == "passed"
1049
-
1050
-
1051
- def _extract_scope_list(report_text, list_name):
1052
- """Extract a nested file list from the report Scope section."""
1053
- in_scope = False
1054
- in_list = False
1055
- list_marker = "- {}:".format(list_name)
1056
- values = []
1057
- for raw_line in report_text.splitlines():
1058
- line = raw_line.strip()
1059
- if line == "## Scope":
1060
- in_scope = True
1061
- continue
1062
- if in_scope and line.startswith("## "):
1063
- break
1064
- if not in_scope:
1065
- continue
1066
- if line == list_marker:
1067
- in_list = True
1068
- continue
1069
- if in_list:
1070
- if raw_line.startswith(" -") or raw_line.startswith("\t-"):
1071
- value = line[2:].strip().strip("`")
1072
- if value and value not in {"{...}", "..."}:
1073
- values.append(value)
1074
- continue
1075
- if line.startswith("- "):
1076
- break
1077
- return values
1078
-
1079
-
1080
- def _scoped_report_fresh(project_root, pointer_artifact, report_path, report_text):
1081
- """Return whether the report is newer than the gate marker and in-scope files."""
1082
- marker_path = os.path.join(
1083
- project_root,
1084
- os.path.dirname(pointer_artifact),
1085
- ".prizmkit-test-started",
1086
- )
1087
- if not os.path.exists(marker_path):
1088
- return False
1089
-
1090
- try:
1091
- report_mtime = os.path.getmtime(report_path)
1092
- marker_mtime = os.path.getmtime(marker_path)
1093
- except OSError:
1094
- return False
1095
-
1096
- if report_mtime < marker_mtime:
1097
- return False
1098
-
1099
- project_root_real = os.path.realpath(project_root)
1100
- source_files = _extract_scope_list(report_text, "In-Scope Source Files")
1101
- test_files = _extract_scope_list(report_text, "In-Scope Test Files")
1102
- if not source_files or not test_files:
1103
- return False
1104
-
1105
- ignored = {"none", "n/a", "not applicable", "(none)", "{...}", "..."}
1106
- for listed_file in source_files + test_files:
1107
- normalized = listed_file.strip().strip("`")
1108
- if not normalized or normalized.lower() in ignored:
1109
- return False
1110
- candidate = (normalized if os.path.isabs(normalized)
1111
- else os.path.join(project_root, normalized))
1112
- candidate_real = os.path.realpath(candidate)
1113
- try:
1114
- if os.path.commonpath([project_root_real, candidate_real]) != project_root_real:
1115
- return False
1116
- except ValueError:
1117
- return False
1118
- if not os.path.isfile(candidate_real):
1119
- return False
1120
- if os.path.getmtime(candidate_real) > report_mtime:
1121
- return False
1122
-
1123
- return True
1124
-
1125
-
1126
- def _validate_scoped_test_report(project_root, artifacts):
1127
- """Validate the feature prizmkit-test pointer and scoped PASS report."""
1128
- pointer_artifacts = [a for a in artifacts
1129
- if a.endswith("/test-report-path.txt")]
1130
- if not pointer_artifacts:
1131
- return False
1132
-
1133
- pointer_artifact = pointer_artifacts[0]
1134
- pointer_path = os.path.join(project_root, pointer_artifact)
1135
- pointed_report = _read_text(pointer_path).strip().splitlines()
1136
- if not pointed_report:
1137
- return False
1138
-
1139
- report_path = pointed_report[0].strip()
1140
- if not os.path.isabs(report_path):
1141
- report_path = os.path.join(project_root, report_path)
1142
- if not os.path.exists(report_path):
1143
- return False
1144
- if not _report_path_matches_artifacts(project_root, report_path, artifacts):
1145
- return False
1146
-
1147
- report_text = _read_text(report_path)
1148
- mode = _extract_scope_field(report_text, "Mode")
1149
- artifact_dir = _extract_scope_field(report_text, "Artifact Dir")
1150
- verdict = _extract_report_verdict(report_text)
1151
-
1152
- expected_artifact_dir = _normalize_artifact_dir(
1153
- os.path.dirname(pointer_artifact)
1154
- )
1155
- actual_artifact_dir = _normalize_artifact_dir(artifact_dir)
1156
-
1157
- return (mode == "this-change" and
1158
- actual_artifact_dir == expected_artifact_dir and
1159
- verdict == "PASS" and
1160
- _boundary_report_sections_valid(project_root, report_text) and
1161
- _scoped_report_fresh(project_root, pointer_artifact,
1162
- report_path, report_text))
1163
-
1164
-
1165
- def _completed_step_artifacts_valid(project_root, skill_key, artifacts):
1166
- """Return whether a completed checkpoint step can be preserved."""
1167
- if not all(_artifact_exists(project_root, a) for a in artifacts):
1168
- return False
1169
- if skill_key == "prizmkit-test":
1170
- return _validate_scoped_test_report(project_root, artifacts)
1171
- return True
1172
-
1173
-
1174
- def generate_checkpoint_definition(sections, pipeline_mode, workflow_type,
1175
- item_id, item_slug, session_id,
1176
- init_done=False):
1177
- """Derive checkpoint step definitions from the assembled sections list.
1178
-
1179
- Args:
1180
- sections: list of (name, content) tuples from assemble_sections()
1181
- pipeline_mode: "lite" | "standard" | "full"
1182
- workflow_type: "feature-pipeline"
1183
- item_id: feature ID (e.g. "F-001")
1184
- item_slug: feature slug (e.g. "001-user-auth")
1185
- session_id: current session ID
1186
- init_done: whether project is already initialized (Phase 0 skip)
1187
-
1188
- Returns:
1189
- dict suitable for writing as workflow-checkpoint.json
1190
- """
1191
- steps = []
1192
- step_counter = 1
1193
- prev_step_id = None
1194
-
1195
- for section_name, _content in sections:
1196
- if section_name not in SECTION_TO_SKILL:
1197
- continue
1198
-
1199
- mapping = SECTION_TO_SKILL[section_name]
1200
-
1201
- if mapping is None:
1202
- # phase-commit -> split into retrospective + committer
1203
- for skill, name, artifacts in _COMMIT_STEPS:
1204
- step_id = "S{:02d}".format(step_counter)
1205
- steps.append({
1206
- "id": step_id,
1207
- "skill": skill,
1208
- "name": name,
1209
- "status": "pending",
1210
- "required_artifacts": _resolve_artifacts(artifacts, item_slug),
1211
- "depends_on": prev_step_id,
1212
- })
1213
- prev_step_id = step_id
1214
- step_counter += 1
1215
- continue
1216
-
1217
- skill, name, artifacts = mapping
1218
- step_id = "S{:02d}".format(step_counter)
1219
-
1220
- status = "pending"
1221
- if init_done and section_name in ("phase0-init", "phase0-test-baseline"):
1222
- status = "skipped"
1223
-
1224
- steps.append({
1225
- "id": step_id,
1226
- "skill": skill,
1227
- "name": name,
1228
- "status": status,
1229
- "required_artifacts": _resolve_artifacts(artifacts, item_slug),
1230
- "depends_on": prev_step_id,
1231
- })
1232
-
1233
- prev_step_id = step_id
1234
- step_counter += 1
1235
-
1236
- return {
1237
- "version": 1,
1238
- "workflow_type": workflow_type,
1239
- "pipeline_mode": pipeline_mode,
1240
- "item_id": item_id,
1241
- "item_slug": item_slug,
1242
- "session_id": session_id,
1243
- "steps": steps,
1244
- }
1245
-
1246
-
1247
- def merge_checkpoint_state(existing, fresh, project_root):
1248
- """Merge existing checkpoint state into a freshly generated definition.
1249
-
1250
- Matching is by skill_key (not step ID), since tier changes across retries
1251
- may shift step IDs.
1252
-
1253
- Merge rules:
1254
- 1. Only keep completed steps whose required_artifacts all exist on disk
1255
- 2. Keep skipped steps unconditionally
1256
- 3. Once a step is NOT completed/skipped, break the dependency chain:
1257
- all subsequent steps reset to pending
1258
- """
1259
- existing_status = {}
1260
- existing_artifacts = {}
1261
- fresh_artifacts = {step["skill"]: step.get("required_artifacts", [])
1262
- for step in fresh.get("steps", [])}
1263
- for step in existing.get("steps", []):
1264
- existing_status[step["skill"]] = step["status"]
1265
- existing_artifacts[step["skill"]] = step.get("required_artifacts", [])
1266
-
1267
- # Determine which completed steps have valid artifacts. Prefer the freshly
1268
- # generated artifact contract so newly added gates invalidate older
1269
- # checkpoint entries that only satisfied a weaker contract.
1270
- valid_completed = set()
1271
- for skill_key, status in existing_status.items():
1272
- if status == "completed":
1273
- artifacts = fresh_artifacts.get(
1274
- skill_key, existing_artifacts.get(skill_key, [])
1275
- )
1276
- if _completed_step_artifacts_valid(project_root,
1277
- skill_key,
1278
- artifacts):
1279
- valid_completed.add(skill_key)
1280
- else:
1281
- LOGGER.warning(
1282
- "Step '%s' was completed but artifacts missing — "
1283
- "resetting to pending", skill_key
1284
- )
1285
- elif status == "skipped":
1286
- valid_completed.add(skill_key)
1287
-
1288
- # Apply to fresh steps; break chain on first non-valid step
1289
- chain_broken = False
1290
- for step in fresh["steps"]:
1291
- if chain_broken:
1292
- step["status"] = "pending"
1293
- continue
1294
-
1295
- prev = existing_status.get(step["skill"])
1296
- if step["skill"] in valid_completed:
1297
- step["status"] = prev # completed or skipped
1298
- else:
1299
- chain_broken = True
1300
- step["status"] = "pending"
1301
-
1302
- return fresh
1303
-
1304
-
1305
- # ============================================================
1306
- # Section Assembly (new modular approach)
1307
- # ============================================================
1308
-
1309
-
1310
- def load_section(sections_dir, name):
1311
- """Load a section file from the sections directory.
1312
-
1313
- Returns the file content as a string, or raises FileNotFoundError.
1314
- """
1315
- path = os.path.join(sections_dir, name)
1316
- if not os.path.isfile(path):
1317
- raise FileNotFoundError("Section file not found: {}".format(path))
1318
- with open(path, "r", encoding="utf-8") as f:
1319
- return f.read()
1320
-
1321
-
1322
- def load_agent_prompts(templates_dir):
1323
- """Load agent prompt templates from agent-prompts/ directory.
1324
-
1325
- Returns a dict of {{AGENT_PROMPT_XXX}} -> prompt content replacements.
1326
- If the directory does not exist, returns an empty dict (backward compat).
1327
- """
1328
- agent_prompts_dir = os.path.join(templates_dir, "agent-prompts")
1329
- if not os.path.isdir(agent_prompts_dir):
1330
- LOGGER.debug("No agent-prompts/ directory found, skipping")
1331
- return {}
1332
-
1333
- # Map filename -> placeholder name
1334
- # e.g. dev-implement.md -> {{AGENT_PROMPT_DEV_IMPLEMENT}}
1335
- prompt_map = {}
1336
- for filename in sorted(os.listdir(agent_prompts_dir)):
1337
- if not filename.endswith(".md"):
1338
- continue
1339
- stem = filename[:-3] # remove .md
1340
- placeholder = "{{{{AGENT_PROMPT_{}}}}}".format(
1341
- stem.upper().replace("-", "_")
1342
- )
1343
- filepath = os.path.join(agent_prompts_dir, filename)
1344
- try:
1345
- with open(filepath, "r", encoding="utf-8") as f:
1346
- prompt_map[placeholder] = f.read().strip()
1347
- LOGGER.debug("Loaded agent prompt: %s -> %s", filename, placeholder)
1348
- except IOError as exc:
1349
- LOGGER.warning("Failed to load agent prompt %s: %s", filename, exc)
1350
-
1351
- return prompt_map
1352
-
1353
-
1354
- def _tier_header(pipeline_mode):
1355
- """Return the tier-specific header and mission description."""
1356
- headers = {
1357
- "lite": (
1358
- "# Dev-Pipeline Session Bootstrap — Tier 1 (Single Agent)\n",
1359
- "**Tier 1 — Single Agent**: You handle everything directly. "
1360
- "No normal work subagents; the log-size monitor is the only "
1361
- "allowed persistent background subagent. No TeamCreate.\n",
1362
- ),
1363
- "standard": (
1364
- "# Dev-Pipeline Session Bootstrap — Tier 2 (Dual Agent)\n",
1365
- "**Tier 2 — Dual Agent**: You handle context + planning "
1366
- "directly. Then spawn Dev and Reviewer subagents. Spawn Dev "
1367
- "and Reviewer agents via the Agent tool.\n",
1368
- ),
1369
- "full": (
1370
- "# Dev-Pipeline Session Bootstrap — Tier 3 (Full Team)\n",
1371
- "**Tier 3 — Full Team**: For complex features, use the full "
1372
- "pipeline with Dev + Reviewer agents spawned via the Agent "
1373
- "tool.\n",
1374
- ),
1375
- }
1376
- return headers.get(pipeline_mode, headers["lite"])
1377
-
1378
-
1379
- def _tier_reminders(pipeline_mode, critic_enabled=False):
1380
- """Return tier-specific reminder text."""
1381
- common = [
1382
- "- MANDATORY skills: `/prizmkit-retrospective`, `/prizmkit-committer` "
1383
- "— never skip these",
1384
- "- Build context-snapshot.md FIRST; use it throughout instead of "
1385
- "re-reading files",
1386
- "- `/prizmkit-committer` is mandatory — do NOT skip the commit phase, "
1387
- "and do NOT replace it with manual git commit commands",
1388
- "- Do NOT run `git add`/`git commit` during implementation phases — "
1389
- "all changes are committed once in the commit phase",
1390
- "- If any files remain after the commit, amend the existing commit — "
1391
- "do NOT create a follow-up commit",
1392
- "- When staging files, always use explicit file names — NEVER use "
1393
- "`git add -A` or `git add .`",
1394
- ]
1395
-
1396
- if pipeline_mode == "lite":
1397
- specific = [
1398
- "- Tier 1: you handle everything directly — no normal work "
1399
- "subagents needed; the log-size monitor is the only allowed "
1400
- "persistent background subagent",
1401
- ]
1402
- elif pipeline_mode == "standard":
1403
- specific = [
1404
- "- Tier 2: orchestrator builds context+plan, Analyzer checks "
1405
- "consistency, Dev implements, Reviewer reviews+tests — use "
1406
- "direct Agent spawn for agents",
1407
- "- context-snapshot.md is append-only: orchestrator writes "
1408
- "Sections 1-4, Dev appends Implementation Log, Reviewer "
1409
- "appends Review Notes",
1410
- "- Gate checks enforce Implementation Log and Review Notes are "
1411
- "written before proceeding",
1412
- "- Do NOT use `run_in_background=true` when spawning normal work "
1413
- "subagents; the log-size monitor is the only allowed persistent "
1414
- "background subagent",
1415
- "- On timeout: check snapshot + git diff HEAD → model:lite → "
1416
- "remaining steps only → max 2 retries per phase → "
1417
- "orchestrator fallback",
1418
- ]
1419
- else: # full
1420
- specific = [
1421
- "- Tier 3: full team — Dev (implementation) → Reviewer "
1422
- "(review + test) — spawn agents directly via Agent tool",
1423
- "- context-snapshot.md is append-only: orchestrator writes "
1424
- "Sections 1-4, Dev appends Implementation Log, Reviewer "
1425
- "appends Review Notes",
1426
- "- Gate checks enforce Implementation Log and Review Notes are "
1427
- "written before proceeding",
1428
- "- Do NOT use `run_in_background=true` when spawning normal work "
1429
- "agents; the log-size monitor is the only allowed persistent "
1430
- "background subagent",
1431
- "- On timeout: check snapshot → model:lite → remaining steps "
1432
- "only → max 2 retries → orchestrator fallback",
1433
- ]
1434
-
1435
- lines = ["## Reminders\n"] + specific + common
1436
- return "\n".join(lines) + "\n"
1437
-
1438
-
1439
- def assemble_sections(pipeline_mode, sections_dir, init_done, is_resume,
1440
- critic_enabled, browser_enabled, retry_count=0,
1441
- browser_tool="auto"):
1442
- """Assemble prompt sections based on tier and conditions.
1443
-
1444
- Uses Python code for conditional logic instead of regex-based
1445
- template blocks. Each section is loaded from a separate .md file.
1446
-
1447
- Returns a list of (section_name, content) tuples in order.
1448
- """
1449
- sections = []
1450
-
1451
- # --- Header ---
1452
- title, tier_desc = _tier_header(pipeline_mode)
1453
- sections.append(("header", title))
1454
-
1455
- # --- Session Context ---
1456
- sections.append(("session-context",
1457
- load_section(sections_dir, "session-context.md")))
1458
-
1459
- # --- Mission ---
1460
- mission = (
1461
- "## Your Mission\n\n"
1462
- "You are the **session orchestrator**. Implement Feature "
1463
- "{{FEATURE_ID}}: \"{{FEATURE_TITLE}}\".\n\n"
1464
- "**CRITICAL**: You MUST NOT exit until ALL work is complete "
1465
- "and committed."
1466
- )
1467
- if pipeline_mode != "lite":
1468
- mission += (
1469
- " When you spawn normal work subagents, wait for each to finish "
1470
- "(run_in_background=false). The log-size monitor is the only "
1471
- "allowed persistent background subagent."
1472
- )
1473
- if pipeline_mode == "full":
1474
- mission += (
1475
- " Do NOT spawn work agents in background and exit — "
1476
- "that kills the session."
1477
- )
1478
- mission += "\n\n" + tier_desc
1479
- sections.append(("mission", mission))
1480
-
1481
- # --- Task Contract: single source of current scope and gates ---
1482
- sections.append(("task-contract",
1483
- load_section(sections_dir, "task-contract.md")))
1484
-
1485
- # --- Feature Context (XML-wrapped, optimization 3) ---
1486
- sections.append(("feature-context",
1487
- load_section(sections_dir, "feature-context.md")))
1488
-
1489
- # --- Context Budget Rules ---
1490
- sections.append(("context-budget-rules",
1491
- load_section(sections_dir, "context-budget-rules.md")))
1492
-
1493
- # --- Log Size Awareness (only disabled when MAX_LOG_SIZE is explicitly 0) ---
1494
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
1495
- if max_log_size != 0 and os.path.isfile(os.path.join(sections_dir, "log-size-awareness.md")):
1496
- sections.append(("log-size-awareness",
1497
- load_section(sections_dir, "log-size-awareness.md")))
1498
-
1499
- # --- Directory Convention (tier-specific) ---
1500
- if pipeline_mode == "lite":
1501
- dc_file = "directory-convention-lite.md"
1502
- elif pipeline_mode == "standard":
1503
- dc_file = "directory-convention-agent.md"
1504
- else:
1505
- dc_file = "directory-convention-full.md"
1506
- sections.append(("directory-convention",
1507
- load_section(sections_dir, dc_file)))
1508
-
1509
- # --- Subagent Timeout Recovery (only for agent tiers) ---
1510
- if pipeline_mode in ("standard", "full"):
1511
- sections.append(("timeout-recovery",
1512
- load_section(sections_dir,
1513
- "subagent-timeout-recovery.md")))
1514
-
1515
- # --- Checkpoint System ---
1516
- checkpoint_section_path = os.path.join(sections_dir, "checkpoint-system.md")
1517
- if os.path.isfile(checkpoint_section_path):
1518
- sections.append(("checkpoint-system",
1519
- load_section(sections_dir, "checkpoint-system.md")))
1520
-
1521
- # --- Execution header ---
1522
- sections.append(("execution-header", "---\n\n## Execution\n"))
1523
-
1524
- # --- Phase 0: Init or Test Baseline ---
1525
- if not init_done:
1526
- sections.append(("phase0-init",
1527
- load_section(sections_dir, "phase0-init.md")))
1528
- else:
1529
- if pipeline_mode in ("standard", "full"):
1530
- sections.append(("phase0-test-baseline",
1531
- load_section(sections_dir,
1532
- "phase0-test-baseline.md")))
1533
- else:
1534
- sections.append(("phase0-skip",
1535
- "### Phase 0: SKIP (already initialized)\n"))
1536
-
1537
- # --- Context Snapshot + Plan (tier-dependent) ---
1538
- if pipeline_mode == "full":
1539
- # Tier 3: full specify + plan workflow
1540
- sections.append(("phase-specify-plan",
1541
- load_section(sections_dir,
1542
- "phase-specify-plan-full.md")))
1543
- else:
1544
- # Tier 1 & 2: separate context snapshot + plan
1545
- snapshot_base = load_section(sections_dir,
1546
- "phase-context-snapshot-base.md")
1547
- if pipeline_mode == "lite":
1548
- snapshot_suffix = load_section(
1549
- sections_dir, "phase-context-snapshot-lite-suffix.md")
1550
- else:
1551
- snapshot_suffix = load_section(
1552
- sections_dir, "phase-context-snapshot-agent-suffix.md")
1553
- sections.append(("phase-context-snapshot",
1554
- snapshot_base + "\n" + snapshot_suffix))
1555
-
1556
- if pipeline_mode == "lite":
1557
- sections.append(("phase-plan",
1558
- load_section(sections_dir,
1559
- "phase-plan-lite.md")))
1560
- else:
1561
- sections.append(("phase-plan",
1562
- load_section(sections_dir,
1563
- "phase-plan-agent.md")))
1564
-
1565
- # --- Critic: Plan Challenge (only if critic enabled) ---
1566
- if critic_enabled:
1567
- if pipeline_mode == "full":
1568
- sections.append(("phase-critic-plan",
1569
- load_section(sections_dir,
1570
- "phase-critic-plan-full.md")))
1571
- else:
1572
- sections.append(("phase-critic-plan",
1573
- load_section(sections_dir,
1574
- "phase-critic-plan.md")))
1575
-
1576
- # --- Implement (tier-dependent) ---
1577
- if pipeline_mode == "lite":
1578
- sections.append(("phase-implement",
1579
- load_section(sections_dir,
1580
- "phase-implement-lite.md")))
1581
- elif pipeline_mode == "full":
1582
- sections.append(("phase-implement",
1583
- load_section(sections_dir,
1584
- "phase-implement-full.md")))
1585
- else:
1586
- sections.append(("phase-implement",
1587
- load_section(sections_dir,
1588
- "phase-implement-agent.md")))
1589
-
1590
- # --- Test Failure Recovery Protocol (tier-specific) ---
1591
- if pipeline_mode == "lite":
1592
- sections.append(("test-failure-recovery",
1593
- load_section(sections_dir,
1594
- "test-failure-recovery-lite.md")))
1595
- else:
1596
- sections.append(("test-failure-recovery",
1597
- load_section(sections_dir,
1598
- "test-failure-recovery-agent.md")))
1599
-
1600
- # --- Scoped Feature Test Gate ---
1601
- sections.append(("phase-prizmkit-test",
1602
- load_section(sections_dir,
1603
- "phase-prizmkit-test.md")))
1604
-
1605
- # Verification Gates are included in Task Contract. Keep AC in one place so
1606
- # background context and implementation prompts cannot redefine scope.
1607
- # --- Review (only for agent tiers) ---
1608
- if pipeline_mode == "full":
1609
- sections.append(("phase-review",
1610
- load_section(sections_dir,
1611
- "phase-review-full.md")))
1612
- elif pipeline_mode == "standard":
1613
- sections.append(("phase-review",
1614
- load_section(sections_dir,
1615
- "phase-review-agent.md")))
1616
-
1617
- # --- Browser Verification (conditional, tool-aware) ---
1618
- if browser_enabled:
1619
- if browser_tool == "opencli":
1620
- browser_section_file = "phase-browser-verification-opencli.md"
1621
- elif browser_tool == "playwright-cli":
1622
- browser_section_file = "phase-browser-verification.md"
1623
- else:
1624
- # "auto" or unknown → let AI choose at runtime
1625
- browser_section_file = "phase-browser-verification-auto.md"
1626
- sections.append(("phase-browser",
1627
- load_section(sections_dir,
1628
- browser_section_file)))
1629
-
1630
- # --- Commit (tier-dependent) ---
1631
- if pipeline_mode == "full":
1632
- sections.append(("phase-commit",
1633
- load_section(sections_dir,
1634
- "phase-commit-full.md")))
1635
- else:
1636
- sections.append(("phase-commit",
1637
- load_section(sections_dir,
1638
- "phase-commit.md")))
1639
-
1640
- # --- Critical Paths ---
1641
- if pipeline_mode == "lite":
1642
- cp_file = "critical-paths-lite.md"
1643
- elif pipeline_mode == "full":
1644
- cp_file = "critical-paths-full.md"
1645
- else:
1646
- cp_file = "critical-paths-agent.md"
1647
- sections.append(("critical-paths",
1648
- load_section(sections_dir, cp_file)))
1649
-
1650
- # --- Failure Capture ---
1651
- sections.append(("failure-capture",
1652
- load_section(sections_dir, "failure-capture.md")))
1653
-
1654
- # --- Reminders ---
1655
- sections.append(("reminders",
1656
- _tier_reminders(pipeline_mode, critic_enabled)))
1657
-
1658
- return sections
1659
-
1660
-
1661
- def render_from_sections(sections, replacements):
1662
- """Join assembled sections and replace all {{PLACEHOLDER}} variables.
1663
-
1664
- No regex-based conditional block processing needed — all conditions
1665
- were resolved during assembly.
1666
- """
1667
- content = "\n".join(text for _, text in sections)
1668
-
1669
- # Replace all placeholders — run twice to handle agent prompt templates
1670
- # that contain their own {{PLACEHOLDER}} variables. First pass injects
1671
- # agent prompt content (e.g. {{AGENT_PROMPT_DEV_IMPLEMENT}} expands to a
1672
- # block containing {{FEATURE_ID}}). Second pass replaces the inner vars.
1673
- for _pass in range(2):
1674
- for placeholder, value in replacements.items():
1675
- content = content.replace(placeholder, value)
1676
-
1677
- return content
1678
-
1679
-
1680
- # ============================================================
1681
- # Rendered output validation (optimization 7)
1682
- # ============================================================
1683
-
1684
-
1685
- def validate_rendered(content):
1686
- """Validate rendered prompt content for completeness.
1687
-
1688
- Checks:
1689
- 1. No unreplaced {{PLACEHOLDER}} variables remain
1690
- 2. No unclosed conditional blocks (legacy {{IF_xxx}} tags)
1691
- 3. Required sections present
1692
-
1693
- Returns (is_valid, warnings, errors) tuple.
1694
- """
1695
- warnings = []
1696
- errors = []
1697
-
1698
- # Check for unreplaced placeholders (excluding code blocks that may
1699
- # contain literal double braces like Jinja or Go templates)
1700
- unreplaced = re.findall(r"\{\{[A-Z][A-Z_0-9]+\}\}", content)
1701
- if unreplaced:
1702
- # Deduplicate
1703
- unique = sorted(set(unreplaced))
1704
- warnings.append(
1705
- "Unreplaced placeholders: {}".format(", ".join(unique))
1706
- )
1707
-
1708
- # Check for unclosed conditional blocks (legacy)
1709
- unclosed_if = re.findall(
1710
- r"\{\{(?:IF|END_IF)_[A-Z_]+\}\}", content
1711
- )
1712
- if unclosed_if:
1713
- unique = sorted(set(unclosed_if))
1714
- errors.append(
1715
- "Unclosed conditional blocks: {}".format(", ".join(unique))
1716
- )
1717
-
1718
- # Check required sections exist
1719
- required_markers = [
1720
- ("## Your Mission", "Mission section"),
1721
- ("## Execution", "Execution section"),
1722
- ("## Failure Capture", "Failure Capture Protocol"),
1723
- ]
1724
- for marker, label in required_markers:
1725
- if marker not in content:
1726
- errors.append("Missing required section: {}".format(label))
1727
-
1728
- is_valid = len(errors) == 0
1729
-
1730
- # Log results
1731
- for w in warnings:
1732
- LOGGER.warning("VALIDATE: %s", w)
1733
- for e in errors:
1734
- LOGGER.error("VALIDATE: %s", e)
1735
-
1736
- return is_valid, warnings, errors
1737
-
1738
-
1739
- def build_replacements(args, feature, features, global_context, script_dir):
1740
- """Build the full dict of placeholder -> replacement value."""
1741
- project_root = resolve_project_root(script_dir)
1742
-
1743
- # Resolve paths - platform-aware agent/team resolution
1744
- platform = os.environ.get("PRIZMKIT_PLATFORM", "")
1745
- home_dir = os.path.expanduser("~")
1746
-
1747
- # Auto-detect platform if not set
1748
- if not platform:
1749
- has_codex = os.path.isdir(os.path.join(project_root, ".codex", "agents"))
1750
- has_claude = os.path.isdir(os.path.join(project_root, ".claude", "agents"))
1751
- has_codebuddy = os.path.isdir(os.path.join(project_root, ".codebuddy", "agents"))
1752
- if has_codex:
1753
- platform = "codex"
1754
- elif has_claude:
1755
- platform = "claude"
1756
- elif has_codebuddy:
1757
- platform = "codebuddy"
1758
- else:
1759
- raise RuntimeError(
1760
- "PrizmKit agents not found. None of .codex/agents/, .claude/agents/, or .codebuddy/agents/ exists. "
1761
- "Run `npx prizmkit install` first, or set PRIZMKIT_PLATFORM=codex|claude|codebuddy explicitly."
1762
- )
1763
-
1764
- if platform == "claude":
1765
- # Claude Code: agents in .claude/agents/, no native team config
1766
- agents_dir = os.path.join(project_root, ".claude", "agents")
1767
- team_config_path = os.path.join(
1768
- project_root, ".claude", "team-info.json",
1769
- )
1770
- elif platform == "codex":
1771
- # Codex: agents and team metadata are project-local references.
1772
- agents_dir = os.path.join(project_root, ".codex", "agents")
1773
- team_config_path = os.path.join(
1774
- project_root, ".codex", "team-info.json",
1775
- )
1776
- else:
1777
- # CodeBuddy: agents in .codebuddy/agents/, team in ~/.codebuddy/teams/
1778
- agents_dir = os.path.join(project_root, ".codebuddy", "agents")
1779
- team_config_path = os.path.join(
1780
- home_dir, ".codebuddy", "teams", "prizm-dev-team", "config.json",
1781
- )
1782
-
1783
- # Agent definitions are native .toml for Codex and .md for Claude/CodeBuddy.
1784
- agent_ext = ".toml" if platform == "codex" else ".md"
1785
- dev_subagent = os.path.join(
1786
- agents_dir, f"prizm-dev-team-dev{agent_ext}",
1787
- )
1788
- reviewer_subagent = os.path.join(
1789
- agents_dir, f"prizm-dev-team-reviewer{agent_ext}",
1790
- )
1791
- critic_subagent = os.path.join(
1792
- agents_dir, f"prizm-dev-team-critic{agent_ext}",
1793
- )
1794
-
1795
- # Verify agent files actually exist — missing files cause confusing
1796
- # errors when the AI session tries to read them later.
1797
- for agent_path, agent_name in [
1798
- (dev_subagent, "dev agent"),
1799
- (reviewer_subagent, "reviewer agent"),
1800
- ]:
1801
- if not os.path.isfile(agent_path):
1802
- LOGGER.warning(
1803
- "Agent file not found: %s (%s). "
1804
- "Subagent spawning may fail. "
1805
- "Run `npx prizmkit install` to reinstall agent definitions.",
1806
- agent_path, agent_name,
1807
- )
1808
- # Validator scripts - check if they exist in .codebuddy/scripts/, otherwise use .prizmkit/dev-pipeline/scripts/
1809
- validator_scripts_dir = os.path.join(project_root, ".prizmkit", "dev-pipeline", "scripts")
1810
- init_script_path = os.path.join(validator_scripts_dir, "init-dev-team.py")
1811
-
1812
- # Session status path (relative to project root)
1813
- session_status_path = os.path.join(
1814
- ".prizmkit", "state", "features", args.feature_id,
1815
- "sessions", args.session_id, "session-status.json",
1816
- )
1817
- # Make it absolute from project root
1818
- session_status_abs = os.path.join(project_root, session_status_path)
1819
-
1820
- # Compute feature slug for per-feature directory naming
1821
- feature_slug = compute_feature_slug(
1822
- args.feature_id, feature.get("title", "")
1823
- )
1824
-
1825
- # Detect project state
1826
- init_done = detect_init_status(project_root)
1827
- artifacts = detect_existing_artifacts(project_root, feature_slug)
1828
- complexity = feature.get(
1829
- "estimated_complexity",
1830
- feature.get("complexity", "medium"),
1831
- )
1832
- if args.mode:
1833
- pipeline_mode = args.mode
1834
- else:
1835
- pipeline_mode = determine_pipeline_mode(complexity)
1836
-
1837
- # Auto-detect resume: if all planning artifacts exist and resume_phase
1838
- # is "null" (fresh start), skip to Phase 6
1839
- effective_resume = args.resume_phase
1840
- if effective_resume == "null" and artifacts["all_complete"]:
1841
- effective_resume = "6"
1842
-
1843
- # Determine critic enablement (priority: CLI > env > feature field > default)
1844
- critic_env = os.environ.get("ENABLE_CRITIC", "").lower()
1845
- if args.critic is not None:
1846
- critic_enabled = args.critic == "true"
1847
- elif critic_env in ("true", "1"):
1848
- critic_enabled = True
1849
- elif critic_env in ("false", "0"):
1850
- critic_enabled = False
1851
- else:
1852
- critic_enabled = bool(feature.get("critic", False))
1853
-
1854
- # Determine critic count (from feature field, default 1)
1855
- # Multi-critic voting (3) must be explicitly set by the user in .prizmkit/plans/feature-list.json
1856
- critic_count = feature.get("critic_count", 1)
1857
-
1858
- # Guard: if critic enabled but agent file missing, force disable and warn
1859
- if critic_enabled and not os.path.isfile(critic_subagent):
1860
- LOGGER.warning(
1861
- "Critic enabled but agent file not found: %s. "
1862
- "Critic phases will be SKIPPED. "
1863
- "Run `npx prizmkit install` to install agent definitions.",
1864
- critic_subagent,
1865
- )
1866
- critic_enabled = False
1867
-
1868
- # Guard: if critic enabled but tier doesn't support it (lite), warn and disable
1869
- if critic_enabled and pipeline_mode == "lite":
1870
- LOGGER.warning(
1871
- "Critic enabled for feature %s but pipeline_mode='lite' (tier1) "
1872
- "does not support critic phases. Critic will be SKIPPED. "
1873
- "Use estimated_complexity='high' or pass --mode standard/full.",
1874
- args.feature_id,
1875
- )
1876
- critic_enabled = False
1877
-
1878
- # Browser interaction - extract from feature if present
1879
- browser_interaction = feature.get("browser_interaction")
1880
- browser_enabled = False
1881
- browser_verify_steps = ""
1882
- browser_tool = "auto" # default: AI chooses at runtime
1883
-
1884
- browser_verify_env = os.environ.get("BROWSER_VERIFY", "").lower()
1885
- if browser_verify_env == "false":
1886
- browser_interaction = None
1887
-
1888
- if browser_interaction and isinstance(browser_interaction, bool):
1889
- # Simple boolean: browser verification enabled, no specific goals
1890
- browser_enabled = True
1891
- browser_tool = "auto"
1892
- browser_verify_steps = (
1893
- " # (no specific verify goals — explore the app and "
1894
- "verify the feature works as expected)")
1895
- elif browser_interaction and isinstance(browser_interaction, dict):
1896
- # Extract tool preference (playwright-cli / opencli / auto)
1897
- browser_tool = browser_interaction.get("tool", "auto")
1898
- if browser_tool not in ("playwright-cli", "opencli", "auto"):
1899
- LOGGER.warning(
1900
- "Unknown browser_interaction.tool '%s', defaulting to 'auto'",
1901
- browser_tool,
1902
- )
1903
- browser_tool = "auto"
1904
-
1905
- # browser_interaction only needs verify_steps — AI auto-detects
1906
- # dev server command, URL, and port from project config
1907
- steps = browser_interaction.get("verify_steps", [])
1908
- if steps:
1909
- browser_enabled = True
1910
- browser_verify_steps = "\n".join(
1911
- " # Goal {}: {}".format(i + 1, step)
1912
- for i, step in enumerate(steps)
1913
- )
1914
- elif browser_interaction.get("url") or browser_interaction.get("enabled", True):
1915
- # Backward compat: old format had url/setup_command fields
1916
- browser_enabled = True
1917
- browser_verify_steps = (
1918
- " # (no specific verify goals — explore the app and "
1919
- "verify the feature works as expected)")
1920
-
1921
- # Auto-detect test commands from project structure
1922
- test_commands = detect_test_command_list(project_root)
1923
- test_cmd = format_powershell_test_commands(test_commands)
1924
- if not test_cmd:
1925
- test_cmd = "(auto-detection found no standard test commands; manually specify TEST_CMD)"
1926
-
1927
- # Optionally extract baseline failures from test execution
1928
- baseline_failures = ""
1929
- if args.extract_baselines:
1930
- baseline_test_commands = format_baseline_test_commands(test_commands)
1931
- if baseline_test_commands:
1932
- baseline_failures = extract_baseline_failures(baseline_test_commands, project_root)
1933
-
1934
- # Extract coverage target from feature.testing field (new in v2)
1935
- coverage_target = "80" # Default coverage target
1936
- testing_config = feature.get("testing", {})
1937
- if isinstance(testing_config, dict):
1938
- coverage_target = str(testing_config.get("coverage_target", 80))
1939
-
1940
- # Detect dev server port from package.json
1941
- dev_port = "3000" # Default fallback
1942
- try:
1943
- pkg_path = os.path.join(project_root, "package.json")
1944
- if os.path.isfile(pkg_path):
1945
- with open(pkg_path, "r", encoding="utf-8") as f:
1946
- pkg = json.load(f)
1947
- dev_script = pkg.get("scripts", {}).get("dev", "")
1948
- # Extract -p <port> from dev script
1949
- port_match = re.search(r"-p\s+(\d+)", dev_script)
1950
- if port_match:
1951
- dev_port = port_match.group(1)
1952
- else:
1953
- # Fallback: try NEXT_PUBLIC_SITE_URL from .env files
1954
- for env_file in [".env.local", ".env"]:
1955
- env_path = os.path.join(project_root, env_file)
1956
- if os.path.isfile(env_path):
1957
- with open(env_path, "r", encoding="utf-8") as ef:
1958
- for line in ef:
1959
- m = re.match(
1960
- r"NEXT_PUBLIC_SITE_URL\s*=\s*.*?:([0-9]+)", line.strip()
1961
- )
1962
- if m:
1963
- dev_port = m.group(1)
1964
- break
1965
- if dev_port != "3000":
1966
- break
1967
- except Exception:
1968
- pass # Keep default 3000 on any error
1969
- dev_url = f"http://localhost:{dev_port}"
1970
-
1971
- max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
1972
- if max_log_size == 0:
1973
- poll_interval = DEFAULT_LOG_MONITOR_POLL_INTERVAL
1974
- else:
1975
- poll_interval = _parse_positive_int_env(
1976
- "LOG_MONITOR_POLL_INTERVAL", DEFAULT_LOG_MONITOR_POLL_INTERVAL,
1977
- )
1978
-
1979
- replacements = {
1980
- "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
1981
- "{{RUN_ID}}": args.run_id,
1982
- "{{SESSION_ID}}": args.session_id,
1983
- "{{FEATURE_ID}}": args.feature_id,
1984
- "{{FEATURE_LIST_PATH}}": os.path.abspath(args.feature_list),
1985
- "{{FEATURE_TITLE}}": feature.get("title", ""),
1986
- "{{FEATURE_DESCRIPTION}}": feature.get("description", ""),
1987
- "{{USER_CONTEXT}}": format_user_context(feature.get("user_context", [])),
1988
- "{{ACCEPTANCE_CRITERIA}}": format_acceptance_criteria(
1989
- feature.get("acceptance_criteria", [])
1990
- ),
1991
- "{{COMPLETED_DEPENDENCIES}}": get_completed_dependencies(
1992
- features, feature
1993
- ),
1994
- "{{GLOBAL_CONTEXT}}": format_global_context(global_context, project_root),
1995
- "{{PROJECT_BRIEF}}": _read_project_brief(project_root),
1996
- "{{PLATFORM_CONVENTIONS}}": read_platform_conventions(project_root),
1997
- "{{TEAM_CONFIG_PATH}}": team_config_path,
1998
- "{{DEV_SUBAGENT_PATH}}": dev_subagent,
1999
- "{{REVIEWER_SUBAGENT_PATH}}": reviewer_subagent,
2000
- "{{CRITIC_SUBAGENT_PATH}}": critic_subagent,
2001
- "{{INIT_SCRIPT_PATH}}": init_script_path,
2002
- "{{SESSION_STATUS_PATH}}": session_status_abs,
2003
- "{{PROJECT_ROOT}}": project_root,
2004
- "{{PIPELINE_DIR}}": ".prizmkit\\dev-pipeline",
2005
- "{{FEATURE_SLUG}}": feature_slug,
2006
- "{{CHECKPOINT_PATH}}": os.path.join(
2007
- ".prizmkit", "specs", feature_slug, "workflow-checkpoint.json",
2008
- ),
2009
- "{{PIPELINE_MODE}}": pipeline_mode,
2010
- "{{COMPLEXITY}}": complexity,
2011
- "{{CRITIC_ENABLED}}": "true" if critic_enabled else "false",
2012
- "{{CRITIC_COUNT}}": str(critic_count),
2013
- "{{INIT_DONE}}": "true" if init_done else "false",
2014
- "{{HAS_SPEC}}": "true" if artifacts["has_spec"] else "false",
2015
- "{{HAS_PLAN}}": "true" if artifacts["has_plan"] else "false",
2016
- "{{BROWSER_VERIFY_STEPS}}": browser_verify_steps,
2017
- "{{BROWSER_TOOL}}": browser_tool,
2018
- "{{AC_CHECKLIST}}": format_ac_checklist(
2019
- feature.get("acceptance_criteria", [])
2020
- ),
2021
- "{{TEST_CMD}}": test_cmd,
2022
- "{{BASELINE_FAILURES}}": baseline_failures,
2023
- "{{COVERAGE_TARGET}}": coverage_target,
2024
- "{{DEV_PORT}}": dev_port,
2025
- "{{DEV_URL}}": dev_url,
2026
- "{{MAX_LOG_SIZE}}": str(max_log_size),
2027
- "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
2028
- "{{LOG_MONITOR_POLL_INTERVAL}}": str(poll_interval),
2029
- "{{LOG_MONITOR_STATUS_FILE}}": os.path.join(
2030
- ".prizmkit", "state", "features", args.feature_id,
2031
- "sessions", args.session_id, "logs", "log-monitor-status.txt",
2032
- ),
2033
- "{{LOG_MONITOR_BASELINE_FILE}}": os.path.join(
2034
- ".prizmkit", "state", "features", args.feature_id,
2035
- "sessions", args.session_id, "logs", "log-monitor-baseline.txt",
2036
- ),
2037
- "{{SESSION_LOG_PATH}}": os.path.join(
2038
- ".prizmkit", "state", "features", args.feature_id,
2039
- "sessions", args.session_id, "logs", "session.log",
2040
- ),
2041
- "{{LOG_MONITOR_SCRIPT}}": os.path.join(
2042
- ".prizmkit", "dev-pipeline", "scripts", "monitor-log.ps1",
2043
- ),
2044
- }
2045
-
2046
- return replacements, effective_resume, browser_enabled, browser_tool
2047
-
2048
-
2049
- def render_template(template_content, replacements, resume_phase,
2050
- browser_enabled=False, browser_tool="auto"):
2051
- """Render the template by processing conditionals and replacing placeholders."""
2052
- # Step 1: Process fresh_start/resume conditional blocks
2053
- content = process_conditional_blocks(template_content, resume_phase)
2054
-
2055
- # Step 2: Process mode, init, critic, and browser conditional blocks
2056
- pipeline_mode = replacements.get("{{PIPELINE_MODE}}", "standard")
2057
- init_done = replacements.get("{{INIT_DONE}}", "false") == "true"
2058
- critic_enabled = replacements.get("{{CRITIC_ENABLED}}", "false") == "true"
2059
- content = process_mode_blocks(content, pipeline_mode, init_done, critic_enabled,
2060
- browser_enabled, browser_tool)
2061
-
2062
- # Step 3: Replace all {{PLACEHOLDER}} variables (two passes for nested
2063
- # agent prompt templates that may contain their own placeholders)
2064
- for _pass in range(2):
2065
- for placeholder, value in replacements.items():
2066
- content = content.replace(placeholder, value)
2067
-
2068
- return content
2069
-
2070
-
2071
- def write_output(output_path, content):
2072
- """Write the rendered content to the output file."""
2073
- abs_path = os.path.abspath(output_path)
2074
- output_dir = os.path.dirname(abs_path)
2075
- if output_dir and not os.path.isdir(output_dir):
2076
- try:
2077
- os.makedirs(output_dir, exist_ok=True)
2078
- except OSError as e:
2079
- return "Cannot create output directory: {}".format(str(e))
2080
- try:
2081
- with open(abs_path, "w", encoding="utf-8") as f:
2082
- f.write(content)
2083
- except IOError as e:
2084
- return "Cannot write output file: {}".format(str(e))
2085
- return None
2086
-
2087
-
2088
- def emit_failure(message):
2089
- """Emit standardized failure JSON and exit."""
2090
- print(json.dumps({"success": False, "error": message}, indent=2, ensure_ascii=False))
2091
- sys.exit(1)
2092
-
2093
-
2094
- def main():
2095
- args = parse_args()
2096
-
2097
- # Resolve script directory
2098
- script_dir = os.path.dirname(os.path.abspath(__file__))
2099
- templates_dir = os.path.join(script_dir, "..", "templates")
2100
- sections_dir = os.path.join(templates_dir, "sections")
2101
-
2102
- # Load feature list early (needed for both code paths)
2103
- feature_list_data, err = load_json_file(args.feature_list)
2104
- if err:
2105
- emit_failure("Feature list error: {}".format(err))
2106
-
2107
- features = feature_list_data.get("features")
2108
- if not isinstance(features, list):
2109
- emit_failure("Feature list does not contain a 'features' array")
2110
-
2111
- feature = find_feature(features, args.feature_id)
2112
- if feature is None:
2113
- emit_failure(
2114
- "Feature '{}' not found in feature list".format(args.feature_id)
2115
- )
2116
-
2117
- global_context = feature_list_data.get("global_context", {})
2118
- if not isinstance(global_context, dict):
2119
- global_context = {}
2120
-
2121
- # Build replacements (shared by both code paths)
2122
- replacements, effective_resume, browser_enabled, browser_tool = build_replacements(
2123
- args, feature, features, global_context, script_dir
2124
- )
2125
-
2126
- # Load agent prompt templates and merge into replacements
2127
- agent_prompt_replacements = load_agent_prompts(templates_dir)
2128
- replacements.update(agent_prompt_replacements)
2129
-
2130
- # Extract state needed for assembly
2131
- pipeline_mode = replacements.get("{{PIPELINE_MODE}}", "lite")
2132
- init_done = replacements.get("{{INIT_DONE}}", "false") == "true"
2133
- is_resume = effective_resume != "null"
2134
- critic_enabled = replacements.get("{{CRITIC_ENABLED}}", "false") == "true"
2135
-
2136
- # ── Choose rendering path ──────────────────────────────────────────
2137
- use_sections = os.path.isdir(sections_dir) and not args.template
2138
-
2139
- if use_sections:
2140
- # New modular section assembly (code-level conditional logic)
2141
- LOGGER.info("Using section assembly from %s", sections_dir)
2142
- try:
2143
- sections = assemble_sections(
2144
- pipeline_mode, sections_dir, init_done, is_resume,
2145
- critic_enabled, browser_enabled,
2146
- retry_count=int(args.retry_count),
2147
- browser_tool=browser_tool,
2148
- )
2149
- rendered = render_from_sections(sections, replacements)
2150
- except FileNotFoundError as exc:
2151
- LOGGER.warning(
2152
- "Section assembly failed (%s), falling back to legacy "
2153
- "template", exc,
2154
- )
2155
- use_sections = False
2156
-
2157
- if not use_sections:
2158
- # Legacy monolithic template path (backward compatible)
2159
- if args.template:
2160
- template_path = args.template
2161
- else:
2162
- complexity = feature.get(
2163
- "estimated_complexity",
2164
- feature.get("complexity", "medium"),
2165
- )
2166
- _mode = args.mode or determine_pipeline_mode(complexity)
2167
- _tier_file_map = {
2168
- "lite": "bootstrap-tier1.md",
2169
- "standard": "bootstrap-tier2.md",
2170
- "full": "bootstrap-tier3.md",
2171
- }
2172
- _tier_file = _tier_file_map.get(_mode, "bootstrap-tier2.md")
2173
- _tier_path = os.path.join(templates_dir, _tier_file)
2174
- if os.path.isfile(_tier_path):
2175
- template_path = _tier_path
2176
- else:
2177
- template_path = os.path.join(
2178
- templates_dir, "bootstrap-prompt.md"
2179
- )
2180
-
2181
- template_content, err = read_text_file(template_path)
2182
- if err:
2183
- emit_failure("Template error: {}".format(err))
2184
-
2185
- rendered = render_template(
2186
- template_content, replacements, effective_resume, browser_enabled,
2187
- browser_tool
2188
- )
2189
-
2190
- # ── Validate rendered output ───────────────────────────────────────
2191
- is_valid, warnings, errors = validate_rendered(rendered)
2192
- if not is_valid:
2193
- LOGGER.error(
2194
- "Rendered prompt failed validation: %s",
2195
- "; ".join(errors),
2196
- )
2197
- # Continue anyway — a partially valid prompt is better than none
2198
-
2199
- # ── Write output ───────────────────────────────────────────────────
2200
- err = write_output(args.output, rendered)
2201
- if err:
2202
- emit_failure(err)
2203
-
2204
- # ── Generate checkpoint file ──────────────────────────────────────
2205
- project_root = resolve_project_root(
2206
- os.path.dirname(os.path.abspath(__file__))
2207
- )
2208
- feature_slug = replacements.get("{{FEATURE_SLUG}}", "")
2209
- checkpoint_path = ""
2210
-
2211
- if not args.no_checkpoint and use_sections and feature_slug:
2212
- checkpoint = generate_checkpoint_definition(
2213
- sections=sections,
2214
- pipeline_mode=pipeline_mode,
2215
- workflow_type="feature-pipeline",
2216
- item_id=args.feature_id,
2217
- item_slug=feature_slug,
2218
- session_id=args.session_id,
2219
- init_done=init_done,
2220
- )
2221
-
2222
- checkpoint_dir = os.path.join(
2223
- project_root, ".prizmkit", "specs", feature_slug,
2224
- )
2225
- os.makedirs(checkpoint_dir, exist_ok=True)
2226
- checkpoint_path = os.path.join(
2227
- checkpoint_dir, "workflow-checkpoint.json",
2228
- )
2229
-
2230
- # On resume, merge existing completed state (with artifact validation)
2231
- if is_resume and os.path.exists(checkpoint_path):
2232
- try:
2233
- with open(checkpoint_path, "r", encoding="utf-8") as f:
2234
- existing = json.load(f)
2235
- checkpoint = merge_checkpoint_state(
2236
- existing, checkpoint, project_root,
2237
- )
2238
- LOGGER.info("Merged existing checkpoint state from %s",
2239
- checkpoint_path)
2240
- except (json.JSONDecodeError, KeyError) as exc:
2241
- LOGGER.warning(
2242
- "Existing checkpoint corrupted (%s) — generating fresh",
2243
- exc,
2244
- )
2245
-
2246
- with open(checkpoint_path, "w", encoding="utf-8") as f:
2247
- json.dump(checkpoint, f, indent=2, ensure_ascii=False)
2248
- LOGGER.info("Wrote checkpoint to %s", checkpoint_path)
2249
-
2250
- # ── Success JSON ───────────────────────────────────────────────────
2251
- feature_model = feature.get("model", "")
2252
- mode_agent_counts = {"lite": 1, "standard": 3, "full": 3}
2253
- agent_count = mode_agent_counts.get(pipeline_mode, 1)
2254
- critic_count_val = int(replacements.get("{{CRITIC_COUNT}}", "1"))
2255
- if critic_enabled:
2256
- agent_count += critic_count_val
2257
- output = {
2258
- "success": True,
2259
- "output_path": os.path.abspath(args.output),
2260
- "model": feature_model,
2261
- "pipeline_mode": pipeline_mode,
2262
- "agent_count": agent_count,
2263
- "critic_enabled": "true" if critic_enabled else "false",
2264
- "render_mode": "sections" if use_sections else "legacy",
2265
- "validation_warnings": len(warnings),
2266
- "validation_errors": len(errors),
2267
- "checkpoint_path": checkpoint_path,
2268
- }
2269
- print(json.dumps(output, indent=2, ensure_ascii=False))
2270
- sys.exit(0)
2271
-
2272
-
2273
- if __name__ == "__main__":
2274
- try:
2275
- main()
2276
- except KeyboardInterrupt:
2277
- emit_failure("generate-bootstrap-prompt interrupted")
2278
- except SystemExit:
2279
- raise
2280
- except Exception as exc:
2281
- LOGGER.exception("Unhandled exception in generate-bootstrap-prompt")
2282
- emit_failure("Unexpected error: {}".format(str(exc)))