prizmkit 1.1.100 → 1.1.102

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