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
@@ -0,0 +1,1226 @@
1
+ """Parity-focused tests for Python foreground runner helpers."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from types import SimpleNamespace
10
+
11
+ import pytest
12
+
13
+ REPO_ROOT = Path(__file__).resolve().parents[2]
14
+ PIPELINE_ROOT = REPO_ROOT / "dev-pipeline"
15
+ CLI_PATH = PIPELINE_ROOT / "cli.py"
16
+
17
+ if str(PIPELINE_ROOT) not in sys.path:
18
+ sys.path.insert(0, str(PIPELINE_ROOT))
19
+
20
+
21
+ def _python_310_plus() -> str:
22
+ if sys.version_info >= (3, 10):
23
+ return sys.executable
24
+ for executable in ("python3.12", "python3.11", "python3.10"):
25
+ resolved = shutil.which(executable)
26
+ if resolved:
27
+ return resolved
28
+ return sys.executable
29
+
30
+
31
+ PYTHON_310_PLUS = _python_310_plus()
32
+ UNIX_RUNTIME_SNAPSHOT = REPO_ROOT / "tests" / "fixtures" / "dev-pipeline-behavior-snapshots" / "unix-runtime.json"
33
+
34
+
35
+ def _load_unix_runtime_snapshot():
36
+ return json.loads(UNIX_RUNTIME_SNAPSHOT.read_text(encoding="utf-8"))
37
+
38
+
39
+ def _make_paths(tmp_path):
40
+ from prizmkit_runtime.paths import resolve_runtime_paths
41
+
42
+ project = tmp_path / "project"
43
+ (project / ".prizmkit" / "plans").mkdir(parents=True)
44
+ return resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project)
45
+
46
+
47
+ def _write_feature_list(paths):
48
+ paths.feature_plan.write_text(
49
+ json.dumps(
50
+ {
51
+ "$schema": "dev-pipeline-feature-list-v1",
52
+ "project_name": "Fake",
53
+ "features": [
54
+ {"id": "F-001", "title": "Low", "status": "pending", "priority": "low", "dependencies": []},
55
+ {"id": "F-002", "title": "Critical", "status": "pending", "priority": "critical", "dependencies": []},
56
+ {"id": "F-003", "title": "Blocked", "status": "pending", "priority": "high", "dependencies": ["F-001"]},
57
+ ],
58
+ }
59
+ ),
60
+ encoding="utf-8",
61
+ )
62
+
63
+
64
+ def test_retired_shell_runtime_files_are_absent_from_source():
65
+ retired = [
66
+ "run-feature.sh",
67
+ "run-bugfix.sh",
68
+ "run-refactor.sh",
69
+ "run-recovery.sh",
70
+ "launch-feature-daemon.sh",
71
+ "launch-bugfix-daemon.sh",
72
+ "launch-refactor-daemon.sh",
73
+ "reset-feature.sh",
74
+ "reset-bug.sh",
75
+ "reset-refactor.sh",
76
+ "lib/branch.sh",
77
+ "lib/common.sh",
78
+ "lib/heartbeat.sh",
79
+ ]
80
+
81
+ for relative_path in retired:
82
+ assert not (PIPELINE_ROOT / relative_path).exists(), relative_path
83
+
84
+
85
+ def test_python_parity_suite_declares_coverage_for_all_legacy_unix_snapshot_scenarios():
86
+ """Keep the F-011 snapshot as an explicit oracle without reviving shell sources."""
87
+ snapshot = _load_unix_runtime_snapshot()
88
+ suite_text = "\n".join(
89
+ [
90
+ Path(__file__).read_text(encoding="utf-8"),
91
+ (PIPELINE_ROOT / "tests" / "test_unified_cli.py").read_text(encoding="utf-8"),
92
+ (PIPELINE_ROOT / "tests" / "test_auto_skip.py").read_text(encoding="utf-8"),
93
+ ]
94
+ )
95
+ coverage_tokens = {
96
+ "entrypointContracts": {
97
+ "runnerHelpContracts": ["test_cli_help_exposes_python_runtime_command_groups"],
98
+ "runnerMissingListErrors": ["test_python_runner_missing_list_errors_match_snapshot_contract"],
99
+ "recoveryHelpContract": ["test_python_entrypoint_contracts_match_snapshot_contracts"],
100
+ "resetHelpContracts": ["test_python_entrypoint_contracts_match_snapshot_contracts"],
101
+ "daemonStatusContracts": ["test_python_entrypoint_contracts_match_snapshot_contracts"],
102
+ },
103
+ "envResolution": {
104
+ "verboseAndStreamJson": ["test_python_env_resolution_scenarios_match_snapshot_contracts"],
105
+ "effortModelAndCli": ["test_ai_session_command_builders_match_unix_runtime_snapshot"],
106
+ "worktreeIgnored": ["test_runner_environment_enables_worktree_by_default_and_accepts_falsey_opt_out"],
107
+ "autoPushFlag": ["test_successful_worktree_session_merges_cleans_branch_and_honors_auto_push"],
108
+ },
109
+ "aiCliCommands": {
110
+ "claudeCommand": ["test_ai_session_command_builders_match_unix_runtime_snapshot"],
111
+ "codexCommand": ["test_ai_session_command_builders_match_unix_runtime_snapshot"],
112
+ "customWrapperCommand": ["test_ai_session_command_builders_match_unix_runtime_snapshot"],
113
+ },
114
+ "fakeCliOutcomes": {
115
+ "success": ["test_session_dual_writes_progress_and_recovers_backup"],
116
+ "contextOverflowContinuation": ["test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outcome"],
117
+ "infraErrorRetry": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
118
+ "crashFailure": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
119
+ "timeoutStaleKill": ["test_heartbeat_fatal_error_writes_shell_compatible_markers"],
120
+ },
121
+ "artifactShapes": {
122
+ "sessionLogsAndProgress": ["test_session_dual_writes_progress_and_recovers_backup"],
123
+ "homeBackupNormalization": ["test_session_dual_writes_progress_and_recovers_backup"],
124
+ },
125
+ "resetBehavior": {
126
+ "manualResetWarningOrder": ["test_python_reset_warns_before_mutation_and_matches_snapshot_cleanup"],
127
+ "cleanResetDeletesContinuationArtifacts": ["test_python_clean_reset_deletes_continuation_artifacts_for_all_families"],
128
+ },
129
+ "daemonBehavior": {
130
+ "stalePidStatusByTaskType": ["test_python_daemon_stale_pid_matches_snapshot"],
131
+ "controlledStartStop": ["test_python_daemon_controlled_start_status_stop_matches_snapshot"],
132
+ },
133
+ "continuationBehavior": {
134
+ "noProgressStall": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
135
+ "branchAndArtifactReuse": ["test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outcome"],
136
+ },
137
+ "statusUpdaterBehavior": {
138
+ "contextOverflowMetadata": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
139
+ "infraErrorRetryBudget": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
140
+ "crashFailureBudget": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
141
+ "resetCleanMetadataCleanup": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
142
+ "noProgressStall": ["test_status_updater_behavior_matches_unix_runtime_snapshot"],
143
+ },
144
+ }
145
+
146
+ assert set(coverage_tokens) == set(snapshot["requiredScenarios"])
147
+ for section, scenario_names in snapshot["requiredScenarios"].items():
148
+ assert set(coverage_tokens[section]) == set(scenario_names), section
149
+ for scenario_name, tokens in coverage_tokens[section].items():
150
+ assert tokens, f"{section}.{scenario_name} has no coverage tokens"
151
+ for token in tokens:
152
+ assert token in suite_text, f"{section}.{scenario_name} coverage token missing: {token}"
153
+
154
+
155
+ def test_python_runner_missing_list_errors_match_snapshot_contract(tmp_path):
156
+ from prizmkit_runtime.runners import run_pipeline_command
157
+
158
+ snapshot = _load_unix_runtime_snapshot()["entrypointContracts"]["runnerMissingListErrors"]
159
+ for kind in ("feature", "bugfix", "refactor"):
160
+ paths = _make_paths(tmp_path / f"missing-{kind}")
161
+ result = run_pipeline_command(kind, "run", (), paths)
162
+
163
+ assert result.exit_code == snapshot[kind]["exitCode"]
164
+ assert "Plan list not found" in result.render()
165
+
166
+
167
+ def test_python_entrypoint_contracts_match_snapshot_contracts(tmp_path):
168
+ from prizmkit_runtime.commands import handle_runtime_command
169
+ from prizmkit_runtime.daemon import run_daemon_command
170
+ from prizmkit_runtime.paths import resolve_runtime_paths
171
+ from prizmkit_runtime.reset import run_reset_command
172
+
173
+ snapshot = _load_unix_runtime_snapshot()["entrypointContracts"]
174
+ paths = _make_paths(tmp_path / "entrypoints")
175
+
176
+ recovery = handle_runtime_command("recovery", "describe", paths=paths)
177
+ assert recovery.exit_code == snapshot["recoveryHelpContract"]["exitCode"]
178
+ assert "Python runtime command contract" in recovery.render()
179
+ assert "recovery describe" in recovery.render()
180
+
181
+ for kind in ("feature", "bugfix", "refactor"):
182
+ reset = run_reset_command(kind, ("--help",), paths)
183
+ assert reset.exit_code == snapshot["resetHelpContracts"][kind]["exitCode"]
184
+ assert "Usage: prizmkit-runtime reset" in reset.render()
185
+
186
+ daemon_paths = resolve_runtime_paths(
187
+ pipeline_root=PIPELINE_ROOT,
188
+ project_root=tmp_path / "daemon-status" / kind,
189
+ )
190
+ daemon = run_daemon_command(kind, "status", (), daemon_paths)
191
+ payload = json.loads(daemon.render())
192
+ assert daemon.exit_code == snapshot["daemonStatusContracts"][kind]["exitCode"]
193
+ assert payload["running"] is False
194
+
195
+
196
+ def test_python_env_resolution_scenarios_match_snapshot_contracts(tmp_path):
197
+ from prizmkit_runtime.sessions import AISessionConfig, build_ai_session_command, detect_stream_json_support
198
+
199
+ snapshot = _load_unix_runtime_snapshot()["envResolution"]
200
+ prompt = tmp_path / "prompt.md"
201
+ prompt.write_text("hello", encoding="utf-8")
202
+
203
+ claude = build_ai_session_command(
204
+ AISessionConfig(
205
+ cli_command="claude",
206
+ platform="claude",
207
+ model=None,
208
+ prompt_path=prompt,
209
+ cwd=tmp_path,
210
+ verbose=True,
211
+ )
212
+ )
213
+ assert ("--verbose" in claude.argv) is snapshot["verboseAndStreamJson"]["claudeVerboseHasFlag"]
214
+
215
+ codebuddy = build_ai_session_command(
216
+ AISessionConfig(
217
+ cli_command="cbc",
218
+ platform="codebuddy",
219
+ model=None,
220
+ prompt_path=prompt,
221
+ cwd=tmp_path,
222
+ use_stream_json=True,
223
+ )
224
+ )
225
+ assert ("--output-format" in codebuddy.argv and "stream-json" in codebuddy.argv) is snapshot["verboseAndStreamJson"]["codebuddyStreamJsonHasFlag"]
226
+
227
+ codex_support = detect_stream_json_support("codex", "codex", lambda command: "Usage: codex exec --json")
228
+ assert codex_support.enabled is True
229
+ assert codex_support.format == "codex-json"
230
+
231
+
232
+ def test_windows_structural_paths_use_installed_python_runtime_layout(tmp_path):
233
+ from prizmkit_runtime.paths import resolve_runtime_paths
234
+
235
+ installed_pipeline = tmp_path / "project" / ".prizmkit" / "dev-pipeline"
236
+ installed_pipeline.mkdir(parents=True)
237
+
238
+ paths = resolve_runtime_paths(pipeline_root=installed_pipeline)
239
+
240
+ assert paths.layout == "installed"
241
+ assert paths.project_root == tmp_path / "project"
242
+ assert paths.pipeline_root == installed_pipeline.resolve()
243
+ assert all("dev-pipeline-windows" not in str(value) for value in paths.path_values())
244
+ assert all(not str(value).endswith(".ps1") for value in paths.path_values())
245
+
246
+
247
+ def test_windows_process_tree_termination_strategy_is_taskkill_backed_without_powershell(monkeypatch, tmp_path):
248
+ from prizmkit_runtime import processes
249
+ from prizmkit_runtime.processes import ProcessHandle, ProcessSpec
250
+
251
+ calls = []
252
+
253
+ class FakeCompleted:
254
+ returncode = 0
255
+ stdout = ""
256
+ stderr = ""
257
+
258
+ def fake_run(command, **kwargs):
259
+ calls.append((tuple(command), kwargs))
260
+ return FakeCompleted()
261
+
262
+ monkeypatch.setattr(processes.os, "name", "nt")
263
+ monkeypatch.setattr(processes.subprocess, "CREATE_NEW_PROCESS_GROUP", 0x200, raising=False)
264
+ monkeypatch.setattr(processes.subprocess, "run", fake_run)
265
+
266
+ spec = ProcessSpec(command=(sys.executable, "-c", "pass"), cwd=tmp_path)
267
+ handle = ProcessHandle(pid=1234, spec=spec, status="running", popen=None)
268
+ launched_flags = processes._creationflags_for_platform(True)
269
+ handle_result = processes.terminate_process_tree(handle, "test-timeout", grace_seconds=0)
270
+
271
+ assert launched_flags == 0x200
272
+ assert processes.platform_termination_strategy() == ("CREATE_NEW_PROCESS_GROUP", "taskkill /PID <pid> /T /F")
273
+ assert handle_result.terminated is True
274
+ assert handle_result.forced is True
275
+ assert calls[-1][0] == ("taskkill", "/PID", "1234", "/T", "/F")
276
+
277
+ calls.clear()
278
+ monkeypatch.setattr(processes, "is_pid_running", lambda pid: True)
279
+ pid_result = processes.terminate_pid_tree(5678, "daemon-stop", grace_seconds=0)
280
+
281
+ assert pid_result.terminated is True
282
+ assert pid_result.forced is True
283
+ assert calls[-1][0] == ("taskkill", "/PID", "5678", "/T", "/F")
284
+
285
+
286
+ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
287
+ from prizmkit_runtime.runner_models import RunnerEnvironment, family_for, parse_invocation
288
+
289
+ paths = _make_paths(tmp_path)
290
+ family = family_for("feature", paths)
291
+ invocation = parse_invocation(
292
+ family,
293
+ "run",
294
+ ("custom-list.json", "F-2", "--features", "F-003:F-001", "--max-retries", "7", "--max-infra-retries=5", "--mode", "full", "--critic"),
295
+ )
296
+ env = RunnerEnvironment.from_env(
297
+ {
298
+ "STOP_ON_FAILURE": "1",
299
+ "AUTO_PUSH": "true",
300
+ "DEV_BRANCH": "dev/custom",
301
+ "VERBOSE": "1",
302
+ "STALE_KILL_THRESHOLD": "9",
303
+ "MAX_RETRIES": "9",
304
+ "STRICT_BEHAVIOR_CHECK": "false",
305
+ }
306
+ )
307
+
308
+ assert invocation.list_path == Path("custom-list.json")
309
+ assert invocation.item_id == "F-002"
310
+ assert invocation.item_filter == "F-003:F-001"
311
+ assert invocation.max_retries == 7
312
+ assert invocation.max_infra_retries == 5
313
+ assert invocation.mode == "full"
314
+ assert invocation.critic is True
315
+ assert env.stop_on_failure is True
316
+ assert env.auto_push is True
317
+ assert env.dev_branch == "dev/custom"
318
+ assert env.stale_kill_threshold_seconds == 9
319
+ assert env.max_retries == 9
320
+ assert env.strict_behavior_check is False
321
+
322
+
323
+ def test_runner_environment_enables_worktree_by_default_and_accepts_falsey_opt_out():
324
+ from prizmkit_runtime.runner_models import RunnerEnvironment
325
+
326
+ assert RunnerEnvironment.from_env({}).use_worktree is True
327
+ assert RunnerEnvironment.from_env({"USE_WORKTREE": "1"}).use_worktree is True
328
+ assert RunnerEnvironment.from_env({"USE_WORKTREE": "true"}).use_worktree is True
329
+ assert RunnerEnvironment.from_env({"USE_WORKTREE": "unexpected"}).use_worktree is True
330
+
331
+ for falsey in ("0", "false", "no", "off"):
332
+ assert RunnerEnvironment.from_env({"USE_WORKTREE": falsey}).use_worktree is False
333
+
334
+
335
+ def test_status_bridge_preserves_feature_filter_selection(tmp_path):
336
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
337
+ from prizmkit_runtime.runner_status import get_next, start_item, update_item
338
+
339
+ paths = _make_paths(tmp_path)
340
+ _write_feature_list(paths)
341
+ family = family_for("feature", paths)
342
+ invocation = parse_invocation(family, "run", ("--features", "F-002:F-003"))
343
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
344
+
345
+ next_result = get_next(family, invocation, paths.project_root)
346
+ assert next_result.ok
347
+ assert isinstance(next_result.data, dict)
348
+ assert next_result.data["feature_id"] == "F-002"
349
+
350
+ start = start_item(family, invocation, "F-002", paths.project_root)
351
+ assert start.ok
352
+ assert start.data["new_status"] == "in_progress"
353
+
354
+ update = update_item(family, invocation, "F-002", "context_overflow", "F-002-session", paths.project_root, active_dev_branch="dev/F-002", base_branch="main", no_progress_count=0, progress_fingerprint={"head_commit": "abc"})
355
+ assert update.ok
356
+ assert update.data["new_status"] == "in_progress"
357
+ assert update.data["restart_policy"] == "context_overflow_continuation"
358
+ assert update.data["context_overflow_count"] == 1
359
+
360
+
361
+ def test_status_bridge_passes_max_retries_from_invocation_or_environment(monkeypatch, tmp_path):
362
+ import subprocess
363
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
364
+ from prizmkit_runtime.runner_status import status_text
365
+
366
+ paths = _make_paths(tmp_path)
367
+ family = family_for("feature", paths)
368
+ explicit = parse_invocation(family, "run", ("--max-retries", "8"))
369
+ explicit = explicit.__class__(**{**explicit.__dict__, "list_path": paths.feature_plan})
370
+ fallback = parse_invocation(family, "run", ())
371
+ fallback = fallback.__class__(**{**fallback.__dict__, "list_path": paths.feature_plan})
372
+ calls = []
373
+
374
+ def fake_run(command, **kwargs):
375
+ calls.append(tuple(command))
376
+ return subprocess.CompletedProcess(command, 0, stdout="PIPELINE_COMPLETE\n", stderr="")
377
+
378
+ monkeypatch.setattr(subprocess, "run", fake_run)
379
+ monkeypatch.setenv("MAX_RETRIES", "6")
380
+
381
+ status_text(family, explicit, paths.project_root)
382
+ status_text(family, fallback, paths.project_root)
383
+
384
+ first = calls[0]
385
+ second = calls[1]
386
+ assert first[first.index("--max-retries") + 1] == "8"
387
+ assert second[second.index("--max-retries") + 1] == "6"
388
+
389
+
390
+ def test_stop_on_failure_does_not_stop_on_retryable_pending(monkeypatch, tmp_path):
391
+ from prizmkit_runtime import runners
392
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
393
+
394
+ paths = _make_paths(tmp_path)
395
+ family = family_for("feature", paths)
396
+ invocation = parse_invocation(family, "run", ())
397
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
398
+ queue = [
399
+ SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
400
+ SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-002"}), data={"feature_id": "F-002"}, text=""),
401
+ SimpleNamespace(ok=True, stdout="PIPELINE_COMPLETE\n", data=None, text="PIPELINE_COMPLETE"),
402
+ ]
403
+ processed = []
404
+
405
+ monkeypatch.setenv("STOP_ON_FAILURE", "1")
406
+ monkeypatch.setattr(runners, "get_next", lambda *args, **kwargs: queue.pop(0))
407
+
408
+ def fake_process(_family, _invocation, item_id, _paths, *, initial_metadata):
409
+ processed.append(item_id)
410
+ return "pending" if item_id == "F-001" else "completed"
411
+
412
+ monkeypatch.setattr(runners, "_process_item", fake_process)
413
+
414
+ result = runners._run_pipeline(family, invocation, paths)
415
+
416
+ assert processed == ["F-001", "F-002"]
417
+ assert result.completed is True
418
+ assert result.stopped_reason == "pipeline_complete"
419
+ assert result.details == ("F-001:pending", "F-002:completed")
420
+
421
+
422
+ def test_stop_on_failure_still_stops_on_terminal_status(monkeypatch, tmp_path):
423
+ from prizmkit_runtime import runners
424
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
425
+
426
+ paths = _make_paths(tmp_path)
427
+ family = family_for("feature", paths)
428
+ invocation = parse_invocation(family, "run", ())
429
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
430
+
431
+ monkeypatch.setenv("STOP_ON_FAILURE", "1")
432
+ monkeypatch.setattr(
433
+ runners,
434
+ "get_next",
435
+ lambda *args, **kwargs: SimpleNamespace(ok=True, stdout=json.dumps({"feature_id": "F-001"}), data={"feature_id": "F-001"}, text=""),
436
+ )
437
+ monkeypatch.setattr(runners, "_process_item", lambda *args, **kwargs: "failed")
438
+
439
+ result = runners._run_pipeline(family, invocation, paths)
440
+
441
+ assert result.completed is False
442
+ assert result.stopped_reason == "stop_on_failure"
443
+ assert result.last_status == "failed"
444
+
445
+
446
+ def test_prompt_generation_result_preserves_per_item_model(monkeypatch, tmp_path):
447
+ from prizmkit_runtime.runner_models import SessionPaths, family_for, parse_invocation
448
+ from prizmkit_runtime.runner_prompts import generate_prompt
449
+
450
+ paths = _make_paths(tmp_path)
451
+ family = family_for("bugfix", paths)
452
+ invocation = parse_invocation(family, "run", ())
453
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.bugfix_plan})
454
+ session_paths = SessionPaths.for_item(paths.bugfix_state_dir, "B-001", "B-001-session")
455
+
456
+ def fake_run(command, **kwargs):
457
+ assert str(family.prompt_script) in command
458
+ assert "--bug-id" in command
459
+ return subprocess.CompletedProcess(
460
+ command,
461
+ 0,
462
+ stdout=json.dumps(
463
+ {
464
+ "success": True,
465
+ "output_path": str(session_paths.prompt_path),
466
+ "checkpoint_path": str(paths.project_root / ".prizmkit" / "bugfix" / "B-001" / "workflow-checkpoint.json"),
467
+ "model": "per-item-model",
468
+ "pipeline_mode": "standard",
469
+ "agent_count": 3,
470
+ "critic_enabled": False,
471
+ }
472
+ ),
473
+ stderr="",
474
+ )
475
+
476
+ monkeypatch.setattr(subprocess, "run", fake_run)
477
+
478
+ result = generate_prompt(
479
+ family,
480
+ invocation,
481
+ item_id="B-001",
482
+ session_id="B-001-session",
483
+ run_id="run-test",
484
+ retry_count=0,
485
+ session_paths=session_paths,
486
+ project_root=paths.project_root,
487
+ )
488
+
489
+ assert result.model == "per-item-model"
490
+ assert result.output_path == session_paths.prompt_path
491
+ assert result.artifact_path == paths.project_root / ".prizmkit" / "bugfix" / "B-001"
492
+
493
+
494
+ def test_prompt_generation_passes_explicit_execution_root_to_generator(monkeypatch, tmp_path):
495
+ from prizmkit_runtime.runner_models import SessionPaths, family_for, parse_invocation
496
+ from prizmkit_runtime.runner_prompts import generate_prompt
497
+
498
+ paths = _make_paths(tmp_path)
499
+ family = family_for("feature", paths)
500
+ invocation = parse_invocation(family, "run", ())
501
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
502
+ session_paths = SessionPaths.for_item(paths.feature_state_dir, "F-001", "F-001-session")
503
+ execution_root = tmp_path / "worktrees" / "dev-F-001"
504
+
505
+ def fake_run(command, **kwargs):
506
+ assert kwargs["cwd"] == str(paths.project_root)
507
+ assert "--project-root" in command
508
+ assert command[command.index("--project-root") + 1] == str(execution_root)
509
+ return subprocess.CompletedProcess(
510
+ command,
511
+ 0,
512
+ stdout=json.dumps(
513
+ {
514
+ "success": True,
515
+ "output_path": str(session_paths.prompt_path),
516
+ "checkpoint_path": str(execution_root / ".prizmkit" / "specs" / "001-low" / "workflow-checkpoint.json"),
517
+ "model": "",
518
+ "pipeline_mode": "lite",
519
+ "agent_count": 1,
520
+ "critic_enabled": False,
521
+ }
522
+ ),
523
+ stderr="",
524
+ )
525
+
526
+ monkeypatch.setattr(subprocess, "run", fake_run)
527
+
528
+ result = generate_prompt(
529
+ family,
530
+ invocation,
531
+ item_id="F-001",
532
+ session_id="F-001-session",
533
+ run_id="run-test",
534
+ retry_count=0,
535
+ session_paths=session_paths,
536
+ project_root=paths.project_root,
537
+ execution_root=execution_root,
538
+ )
539
+
540
+ assert result.artifact_path == execution_root / ".prizmkit" / "specs" / "001-low"
541
+
542
+
543
+ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",), update_status="completed"):
544
+ from prizmkit_runtime.runner_models import PromptGenerationResult, SessionClassification
545
+
546
+ prompt_calls = []
547
+ launch_cwds = []
548
+ updates = []
549
+ cleanup_calls = []
550
+ merge_calls = []
551
+ wip_calls = []
552
+ status_queue = list(statuses)
553
+
554
+ monkeypatch.setattr(
555
+ runners,
556
+ "load_runtime_config",
557
+ lambda paths: SimpleNamespace(
558
+ ai_client=SimpleNamespace(command="fake-ai", platform="codebuddy"),
559
+ model="fallback-model",
560
+ effort=None,
561
+ ),
562
+ )
563
+ monkeypatch.setattr(runners, "detect_stream_json_support", lambda command, platform: SimpleNamespace(enabled=False))
564
+ monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: SimpleNamespace(ok=True, text="", data={"new_status": "in_progress"}))
565
+ monkeypatch.setattr(runners, "commit_pre_branch_bookkeeping", lambda *args, **kwargs: None)
566
+ monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
567
+ monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
568
+ monkeypatch.setattr(runners, "_session_id", lambda item_id: f"{item_id}-session-{len(prompt_calls) + 1}")
569
+ monkeypatch.setattr(
570
+ runners,
571
+ "run_git_command",
572
+ lambda project_root, args: SimpleNamespace(return_code=0, stdout="base-head\n", stderr="", args=tuple(args)),
573
+ )
574
+
575
+ def fake_generate_prompt(family, invocation, **kwargs):
576
+ prompt_calls.append(kwargs)
577
+ session_paths = kwargs["session_paths"]
578
+ execution_root = kwargs.get("execution_root") or kwargs["project_root"]
579
+ return PromptGenerationResult(
580
+ output_path=session_paths.prompt_path,
581
+ model="per-item-model",
582
+ pipeline_mode="lite",
583
+ agent_count=1,
584
+ critic_enabled=False,
585
+ checkpoint_path=execution_root / ".prizmkit" / "specs" / "001-low" / "workflow-checkpoint.json",
586
+ artifact_path=execution_root / ".prizmkit" / "specs" / "001-low",
587
+ raw={},
588
+ )
589
+
590
+ class FakeLauncher:
591
+ def __init__(self, config):
592
+ self.config = config
593
+
594
+ def run(self):
595
+ launch_cwds.append(self.config.cwd)
596
+ return SimpleNamespace(progress_summary=SimpleNamespace(), fatal_error_code="")
597
+
598
+ def fake_classify(*args, **kwargs):
599
+ status = status_queue.pop(0)
600
+ return SessionClassification(
601
+ status,
602
+ reason=status,
603
+ fatal_error_code="context_overflow" if status == "context_overflow" else "",
604
+ progress_fingerprint={"head_commit": status},
605
+ no_progress_count=0,
606
+ )
607
+
608
+ def fake_update(family, invocation, item_id, session_status, session_id, project_root, **metadata):
609
+ updates.append((session_status, session_id, metadata))
610
+ return SimpleNamespace(ok=True, text="", data={"new_status": update_status})
611
+
612
+ def fake_cleanup(runtime, *, delete_branch=True):
613
+ cleanup_calls.append((runtime, delete_branch))
614
+ return SimpleNamespace(ok=True, reason="")
615
+
616
+ def fake_merge(runtime, *, auto_push=False):
617
+ merge_calls.append((runtime, auto_push))
618
+ return SimpleNamespace(ok=True, reason="", runtime=runtime, results=())
619
+
620
+ def fake_save_wip(runtime):
621
+ wip_calls.append(runtime)
622
+ return SimpleNamespace(ok=True, plan=None, results=())
623
+
624
+ monkeypatch.setattr(runners, "generate_prompt", fake_generate_prompt)
625
+ monkeypatch.setattr(runners, "AISessionLauncher", FakeLauncher)
626
+ monkeypatch.setattr(runners, "classify_session", fake_classify)
627
+ monkeypatch.setattr(runners, "update_item", fake_update)
628
+ monkeypatch.setattr(runners, "guarded_worktree_remove", fake_cleanup, raising=False)
629
+ monkeypatch.setattr(runners, "merge_linked_worktree", fake_merge, raising=False)
630
+ monkeypatch.setattr(runners, "worktree_save_wip", fake_save_wip, raising=False)
631
+ return SimpleNamespace(
632
+ prompt_calls=prompt_calls,
633
+ launch_cwds=launch_cwds,
634
+ updates=updates,
635
+ cleanup_calls=cleanup_calls,
636
+ merge_calls=merge_calls,
637
+ wip_calls=wip_calls,
638
+ )
639
+
640
+
641
+ @pytest.mark.parametrize(("kind", "item_id", "branch"), [("feature", "F-001", "dev/F-001-test"), ("bugfix", "B-001", "bugfix/B-001-test")])
642
+ def test_feature_and_bugfix_default_worktree_launches_from_worktree_and_renders_worktree_root(monkeypatch, tmp_path, kind, item_id, branch):
643
+ from prizmkit_runtime import runners
644
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
645
+
646
+ monkeypatch.setenv("DEV_BRANCH", branch)
647
+ paths = _make_paths(tmp_path / kind)
648
+ family = family_for(kind, paths)
649
+ invocation = parse_invocation(family, "run", ())
650
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan if kind == "feature" else paths.bugfix_plan})
651
+ observed = _install_runner_session_fakes(monkeypatch, runners)
652
+ setup_calls = []
653
+
654
+ def fake_setup(runtime):
655
+ setup_calls.append(runtime)
656
+ runtime.worktree_path.mkdir(parents=True, exist_ok=True)
657
+ return SimpleNamespace(ok=True, runtime=runtime, reason="")
658
+
659
+ monkeypatch.setattr(runners, "ensure_linked_worktree", fake_setup, raising=False)
660
+
661
+ status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
662
+
663
+ expected_worktree = paths.state_dir / "worktrees" / branch.replace("/", "-")
664
+ assert status == "completed"
665
+ assert setup_calls[0].worktree_path == expected_worktree.resolve()
666
+ assert observed.launch_cwds == [expected_worktree.resolve()]
667
+ assert observed.prompt_calls[0]["project_root"] == paths.project_root
668
+ assert observed.prompt_calls[0]["execution_root"] == expected_worktree.resolve()
669
+
670
+
671
+ def test_successful_worktree_session_merges_cleans_branch_and_honors_auto_push(monkeypatch, tmp_path):
672
+ from prizmkit_runtime import runners
673
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
674
+
675
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
676
+ monkeypatch.setenv("AUTO_PUSH", "1")
677
+ paths = _make_paths(tmp_path)
678
+ family = family_for("feature", paths)
679
+ invocation = parse_invocation(family, "run", ())
680
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
681
+ observed = _install_runner_session_fakes(monkeypatch, runners)
682
+ monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""), raising=False)
683
+
684
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
685
+
686
+ assert status == "completed"
687
+ assert observed.merge_calls and observed.merge_calls[0][1] is True
688
+ assert observed.cleanup_calls and observed.cleanup_calls[0][1] is True
689
+
690
+
691
+ def test_worktree_merge_conflict_writes_session_status_and_updates_merge_conflict(monkeypatch, tmp_path):
692
+ from prizmkit_runtime import runners
693
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
694
+
695
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
696
+ paths = _make_paths(tmp_path)
697
+ family = family_for("feature", paths)
698
+ invocation = parse_invocation(family, "run", ())
699
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
700
+ observed = _install_runner_session_fakes(monkeypatch, runners)
701
+ monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""), raising=False)
702
+ monkeypatch.setattr(runners, "merge_linked_worktree", lambda runtime, *, auto_push=False: SimpleNamespace(ok=False, reason="merge_conflict", runtime=runtime, results=()), raising=False)
703
+
704
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
705
+
706
+ assert status == "completed"
707
+ assert observed.updates[-1][0] == "merge_conflict"
708
+ assert observed.cleanup_calls[-1][1] is False
709
+ session_status = json.loads((paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-1" / "session-status.json").read_text(encoding="utf-8"))
710
+ assert session_status["status"] == "merge_conflict"
711
+ assert session_status["task_id"] == "F-001"
712
+ assert session_status["task_type"] == "feature"
713
+ assert session_status["base_branch"] == "main"
714
+ assert session_status["active_dev_branch"] == "dev/F-001-test"
715
+ assert session_status["worktree_path"].endswith("dev-F-001-test")
716
+
717
+
718
+ def test_terminal_worktree_failure_saves_wip_and_removes_worktree_without_deleting_branch(monkeypatch, tmp_path):
719
+ from prizmkit_runtime import runners
720
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
721
+
722
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
723
+ paths = _make_paths(tmp_path)
724
+ family = family_for("feature", paths)
725
+ invocation = parse_invocation(family, "run", ())
726
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
727
+ observed = _install_runner_session_fakes(monkeypatch, runners, statuses=("crashed",), update_status="failed")
728
+ monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""), raising=False)
729
+
730
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
731
+
732
+ assert status == "failed"
733
+ assert observed.wip_calls
734
+ assert observed.cleanup_calls[-1][1] is False
735
+ assert observed.updates[-1][0] == "crashed"
736
+
737
+
738
+ def test_default_worktree_materializes_ignored_support_assets_before_prompt_and_launch(monkeypatch, tmp_path):
739
+ from prizmkit_runtime import runners
740
+ from prizmkit_runtime.paths import resolve_runtime_paths
741
+ from prizmkit_runtime.runner_models import PromptGenerationResult, SessionClassification, family_for, parse_invocation
742
+
743
+ project = tmp_path / "repo"
744
+ project.mkdir()
745
+ subprocess.run(["git", "init", "-b", "main"], cwd=project, check=True, stdout=subprocess.DEVNULL)
746
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=project, check=True)
747
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=project, check=True)
748
+ (project / ".gitignore").write_text(".claude/\n.prizmkit/\n", encoding="utf-8")
749
+ (project / "tracked.txt").write_text("tracked\n", encoding="utf-8")
750
+ subprocess.run(["git", "add", ".gitignore", "tracked.txt"], cwd=project, check=True)
751
+ subprocess.run(["git", "commit", "-m", "base"], cwd=project, check=True, stdout=subprocess.DEVNULL)
752
+
753
+ (project / ".claude" / "agents").mkdir(parents=True)
754
+ (project / ".claude" / "agents" / "prizm-dev-team-dev.md").write_text("dev agent", encoding="utf-8")
755
+ (project / ".claude" / "team-info.json").write_text("{}", encoding="utf-8")
756
+ (project / ".prizmkit" / "prizm-docs").mkdir(parents=True)
757
+ (project / ".prizmkit" / "prizm-docs" / "root.prizm").write_text("PRIZM_VERSION: 1\n", encoding="utf-8")
758
+ (project / ".prizmkit" / "dev-pipeline" / "scripts").mkdir(parents=True)
759
+ (project / ".prizmkit" / "dev-pipeline" / "scripts" / "init-dev-team.py").write_text("# init", encoding="utf-8")
760
+ (project / ".prizmkit" / "config.json").write_text("{}", encoding="utf-8")
761
+ (project / ".prizmkit" / "manifest.json").write_text("{}", encoding="utf-8")
762
+
763
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project)
764
+ family = family_for("feature", paths)
765
+ invocation = parse_invocation(family, "run", ())
766
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
767
+ launch_cwds = []
768
+
769
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-support-assets")
770
+ monkeypatch.setattr(
771
+ runners,
772
+ "load_runtime_config",
773
+ lambda paths: SimpleNamespace(
774
+ ai_client=SimpleNamespace(command="fake-ai", platform="codebuddy"),
775
+ model="fallback-model",
776
+ effort=None,
777
+ ),
778
+ )
779
+ monkeypatch.setattr(runners, "detect_stream_json_support", lambda command, platform: SimpleNamespace(enabled=False))
780
+ monkeypatch.setattr(runners, "start_item", lambda *args, **kwargs: SimpleNamespace(ok=True, text="", data={"new_status": "in_progress"}))
781
+ monkeypatch.setattr(runners, "commit_pre_branch_bookkeeping", lambda *args, **kwargs: None)
782
+ monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
783
+ monkeypatch.setattr(runners, "_session_id", lambda item_id: f"{item_id}-session")
784
+ monkeypatch.setattr(runners, "update_item", lambda *args, **kwargs: SimpleNamespace(ok=True, text="", data={"new_status": "completed"}))
785
+
786
+ def fake_generate_prompt(family, invocation, **kwargs):
787
+ execution_root = kwargs["execution_root"]
788
+ assert execution_root != project
789
+ assert (execution_root / ".claude" / "agents" / "prizm-dev-team-dev.md").is_file()
790
+ assert (execution_root / ".prizmkit" / "prizm-docs" / "root.prizm").is_file()
791
+ assert (execution_root / ".prizmkit" / "config.json").is_file()
792
+ assert (execution_root / ".prizmkit" / "manifest.json").is_file()
793
+ assert (execution_root / ".prizmkit" / "dev-pipeline" / "scripts" / "init-dev-team.py").is_file()
794
+ assert not (execution_root / ".prizmkit" / "dev-pipeline" / "run-feature.sh").exists()
795
+ session_paths = kwargs["session_paths"]
796
+ return PromptGenerationResult(
797
+ output_path=session_paths.prompt_path,
798
+ model="",
799
+ pipeline_mode="lite",
800
+ agent_count=1,
801
+ critic_enabled=False,
802
+ checkpoint_path=execution_root / ".prizmkit" / "specs" / "001-support" / "workflow-checkpoint.json",
803
+ artifact_path=execution_root / ".prizmkit" / "specs" / "001-support",
804
+ raw={},
805
+ )
806
+
807
+ class FakeLauncher:
808
+ def __init__(self, config):
809
+ self.config = config
810
+
811
+ def run(self):
812
+ launch_cwds.append(self.config.cwd)
813
+ return SimpleNamespace(progress_summary=SimpleNamespace(), fatal_error_code="")
814
+
815
+ monkeypatch.setattr(runners, "generate_prompt", fake_generate_prompt)
816
+ monkeypatch.setattr(runners, "AISessionLauncher", FakeLauncher)
817
+ monkeypatch.setattr(
818
+ runners,
819
+ "classify_session",
820
+ lambda *args, **kwargs: SessionClassification(
821
+ "success", reason="success", fatal_error_code="",
822
+ progress_fingerprint={"head_commit": "success"}, no_progress_count=0,
823
+ ),
824
+ )
825
+
826
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
827
+
828
+ assert status == "completed"
829
+ assert launch_cwds and launch_cwds[0] == paths.state_dir / "worktrees" / "dev-F-001-support-assets"
830
+ assert not launch_cwds[0].exists()
831
+
832
+
833
+ def test_use_worktree_false_preserves_main_worktree_branch_launch(monkeypatch, tmp_path):
834
+ from prizmkit_runtime import runners
835
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
836
+
837
+ monkeypatch.setenv("USE_WORKTREE", "0")
838
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
839
+ paths = _make_paths(tmp_path)
840
+ family = family_for("feature", paths)
841
+ invocation = parse_invocation(family, "run", ())
842
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
843
+ observed = _install_runner_session_fakes(monkeypatch, runners)
844
+ plan_names = []
845
+
846
+ def fake_run_git_plan(project_root, plan):
847
+ plan_names.append(plan.name)
848
+ return SimpleNamespace(ok=True, plan=plan, results=())
849
+
850
+ monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
851
+ monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: (_ for _ in ()).throw(AssertionError("worktree setup should not run")), raising=False)
852
+
853
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
854
+
855
+ assert status == "completed"
856
+ assert observed.launch_cwds == [paths.project_root]
857
+ assert observed.prompt_calls[0].get("execution_root") == paths.project_root
858
+ assert "branch_create" in plan_names
859
+ assert "branch_merge" in plan_names
860
+
861
+
862
+ def test_context_overflow_reuses_worktree_and_skips_cleanup_until_terminal_outcome(monkeypatch, tmp_path):
863
+ from prizmkit_runtime import runners
864
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
865
+
866
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-test")
867
+ paths = _make_paths(tmp_path)
868
+ family = family_for("feature", paths)
869
+ invocation = parse_invocation(family, "run", ())
870
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
871
+ observed = _install_runner_session_fakes(monkeypatch, runners, statuses=("context_overflow", "success"))
872
+ setup_calls = []
873
+ monkeypatch.setattr(runners, "ensure_linked_worktree", lambda runtime: (setup_calls.append(runtime) or SimpleNamespace(ok=True, runtime=runtime, reason="")), raising=False)
874
+
875
+ status = runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
876
+
877
+ assert status == "completed"
878
+ assert len(setup_calls) == 1
879
+ assert observed.launch_cwds[0] == observed.launch_cwds[1]
880
+ assert observed.prompt_calls[0]["execution_root"] == observed.prompt_calls[1]["execution_root"]
881
+ assert observed.prompt_calls[0].get("continuation") is None
882
+ assert observed.prompt_calls[1]["continuation"]["previous_session_id"] == "F-001-session-1"
883
+ assert observed.updates[0][0] == "context_overflow"
884
+ assert len(observed.cleanup_calls) == 1
885
+ assert observed.cleanup_calls[0][1] is True
886
+
887
+
888
+ def test_classification_preserves_success_before_context_overflow(tmp_path):
889
+ from prizmkit_runtime.runner_classification import classify_session
890
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
891
+ from prizmkit_runtime.status import ProgressSummary
892
+
893
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
894
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
895
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
896
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
897
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
898
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
899
+ base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
900
+ (tmp_path / "done.txt").write_text("done", encoding="utf-8")
901
+ subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
902
+ subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
903
+ log = tmp_path / "session.log"
904
+ log.write_text("API Error: input exceeds the context window", encoding="utf-8")
905
+
906
+ raw = AISessionResult(
907
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
908
+ exit_code=1,
909
+ timed_out=False,
910
+ termination_reason=None,
911
+ stale_marker=None,
912
+ fatal_error_code="context_overflow",
913
+ progress_summary=ProgressSummary(fatal_error_code="context_overflow"),
914
+ session_log=log,
915
+ backup_log=tmp_path / "backup.log",
916
+ log_recovery=None,
917
+ started_epoch=0,
918
+ ended_epoch=1,
919
+ )
920
+
921
+ classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
922
+ assert classification.session_status == "success"
923
+ assert classification.reason == "commit_since_base"
924
+
925
+
926
+ def test_classification_stalls_after_repeated_no_progress_context_overflow(tmp_path):
927
+ from prizmkit_runtime.runner_classification import classify_session, progress_fingerprint
928
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
929
+ from prizmkit_runtime.status import ProgressSummary
930
+
931
+ log = tmp_path / "session.log"
932
+ log.write_text("PRIZMKIT_FATAL_ERROR=context_overflow", encoding="utf-8")
933
+ raw = AISessionResult(
934
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
935
+ exit_code=1,
936
+ timed_out=False,
937
+ termination_reason=None,
938
+ stale_marker=None,
939
+ fatal_error_code="context_overflow",
940
+ progress_summary=ProgressSummary(fatal_error_code="context_overflow"),
941
+ session_log=log,
942
+ backup_log=tmp_path / "backup.log",
943
+ log_recovery=None,
944
+ started_epoch=0,
945
+ ended_epoch=1,
946
+ )
947
+ previous = progress_fingerprint(tmp_path)
948
+
949
+ classification = classify_session(
950
+ raw,
951
+ project_root=tmp_path,
952
+ base_head="",
953
+ previous_fingerprint=previous,
954
+ previous_no_progress_count=1,
955
+ )
956
+
957
+ assert classification.session_status == "stalled_context_continuation"
958
+ assert classification.no_progress_count == 2
959
+
960
+
961
+ def test_recovery_detector_resolver_prefers_installed_platform_asset(tmp_path):
962
+ from prizmkit_runtime.paths import resolve_runtime_paths
963
+ from prizmkit_runtime.runner_recovery import RecoveryDetectorResolver
964
+
965
+ project = tmp_path / "project"
966
+ agents = project / ".agents" / "skills" / "recovery-workflow" / "scripts"
967
+ claude = project / ".claude" / "command-assets" / "recovery-workflow" / "scripts"
968
+ agents.mkdir(parents=True)
969
+ claude.mkdir(parents=True)
970
+ agent_detector = agents / "detect-recovery-state.py"
971
+ claude_detector = claude / "detect-recovery-state.py"
972
+ agent_detector.write_text("# agent", encoding="utf-8")
973
+ claude_detector.write_text("# claude", encoding="utf-8")
974
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project)
975
+
976
+ assert RecoveryDetectorResolver(paths).resolve() == agent_detector
977
+
978
+
979
+ def test_cli_family_status_uses_python_dispatch_not_transition_description(tmp_path):
980
+ paths = _make_paths(tmp_path)
981
+ _write_feature_list(paths)
982
+ result = subprocess.run(
983
+ [PYTHON_310_PLUS, str(CLI_PATH), "--project-root", str(paths.project_root), "feature", "status"],
984
+ cwd=REPO_ROOT,
985
+ stdout=subprocess.PIPE,
986
+ stderr=subprocess.PIPE,
987
+ text=True,
988
+ check=False,
989
+ )
990
+
991
+ assert result.returncode == 0
992
+ assert "Python feature status" in result.stdout
993
+ assert "Transition command contract" not in result.stdout
994
+
995
+
996
+ def _write_family_case(paths, kind, item_id=""):
997
+ item_id = item_id or {"feature": "F-001", "bugfix": "B-001", "refactor": "R-001"}[kind]
998
+ if kind == "feature":
999
+ paths.feature_plan.write_text(json.dumps({"features": [{"id": item_id, "title": "Reset Thing", "status": "failed", "dependencies": []}]}), encoding="utf-8")
1000
+ state_dir = paths.feature_state_dir
1001
+ plan = paths.feature_plan
1002
+ elif kind == "bugfix":
1003
+ paths.bugfix_plan.write_text(json.dumps({"bugs": [{"id": item_id, "title": "Reset Bug", "description": "d", "severity": "medium", "priority": "medium", "status": "failed"}]}), encoding="utf-8")
1004
+ state_dir = paths.bugfix_state_dir
1005
+ plan = paths.bugfix_plan
1006
+ else:
1007
+ paths.refactor_plan.write_text(json.dumps({"refactors": [{"id": item_id, "title": "Reset Refactor", "description": "d", "complexity": "medium", "priority": "medium", "status": "failed", "dependencies": []}]}), encoding="utf-8")
1008
+ state_dir = paths.refactor_state_dir
1009
+ plan = paths.refactor_plan
1010
+ state_dir.mkdir(parents=True, exist_ok=True)
1011
+ (state_dir / "pipeline.json").write_text("{}", encoding="utf-8")
1012
+ item_state = state_dir / item_id
1013
+ item_state.mkdir(parents=True, exist_ok=True)
1014
+ (item_state / "status.json").write_text(
1015
+ json.dumps(
1016
+ {
1017
+ "status": "in_progress",
1018
+ "retry_count": 2,
1019
+ "infra_error_count": 1,
1020
+ "last_infra_error_session_id": "infra-1",
1021
+ "last_session_id": "sess-1",
1022
+ "sessions": ["sess-1"],
1023
+ "continuation_pending": True,
1024
+ "continuation_reason": "context_overflow",
1025
+ "active_dev_branch": f"{kind}/{item_id}-branch",
1026
+ "continuation_summary_path": str(paths.project_root / ".prizmkit" / ("specs" if kind == "feature" else kind) / item_id / "continuation-summary.md"),
1027
+ }
1028
+ ),
1029
+ encoding="utf-8",
1030
+ )
1031
+ return plan, state_dir, item_state / "status.json"
1032
+
1033
+
1034
+ def test_python_reset_warns_before_mutation_and_matches_snapshot_cleanup(tmp_path):
1035
+ from prizmkit_runtime.reset import run_reset_command
1036
+
1037
+ snapshot = json.loads((REPO_ROOT / "tests" / "fixtures" / "dev-pipeline-behavior-snapshots" / "unix-runtime.json").read_text(encoding="utf-8"))
1038
+ for kind, item_id in [("feature", "F-001"), ("bugfix", "B-001"), ("refactor", "R-001")]:
1039
+ paths = _make_paths(tmp_path / kind)
1040
+ _plan, _state_dir, status_file = _write_family_case(paths, kind, item_id)
1041
+ result = run_reset_command(kind, (item_id,), paths)
1042
+ output = result.render()
1043
+ reset_marker = f"Resetting {item_id} status"
1044
+
1045
+ assert result.exit_code == snapshot["resetBehavior"]["manualResetWarningOrder"][kind]["exitCode"]
1046
+ assert "Continuation pending" in output
1047
+ assert "Pending continuation branch" in output
1048
+ assert output.index("Continuation pending") < output.index(reset_marker)
1049
+ runtime = json.loads(status_file.read_text(encoding="utf-8"))
1050
+ expected_runtime = snapshot["statusUpdaterBehavior"]["resetCleanMetadataCleanup"][kind]["reset"]["runtime"]
1051
+ for key, value in expected_runtime.items():
1052
+ assert runtime.get(key) == value
1053
+ assert "continuation_pending" not in runtime
1054
+
1055
+
1056
+ def test_python_clean_reset_deletes_continuation_artifacts_for_all_families(tmp_path):
1057
+ from prizmkit_runtime.reset import run_reset_command
1058
+
1059
+ snapshot = json.loads((REPO_ROOT / "tests" / "fixtures" / "dev-pipeline-behavior-snapshots" / "unix-runtime.json").read_text(encoding="utf-8"))
1060
+ cases = [("feature", "F-001", "specs", "001-reset-thing"), ("bugfix", "B-001", "bugfix", "B-001"), ("refactor", "R-001", "refactor", "R-001")]
1061
+ for kind, item_id, artifact_root, artifact_name in cases:
1062
+ paths = _make_paths(tmp_path / f"clean-{kind}")
1063
+ _plan, _state_dir, status_file = _write_family_case(paths, kind, item_id)
1064
+ artifact_dir = paths.project_root / ".prizmkit" / artifact_root / artifact_name
1065
+ artifact_dir.mkdir(parents=True, exist_ok=True)
1066
+ continuation = artifact_dir / "continuation-summary.md"
1067
+ continuation.write_text("summary", encoding="utf-8")
1068
+ args = (item_id, "--clean")
1069
+ result = run_reset_command(kind, args, paths)
1070
+
1071
+ assert result.exit_code == snapshot["resetBehavior"]["cleanResetDeletesContinuationArtifacts"][kind]["exitCode"]
1072
+ assert continuation.exists() is False
1073
+ runtime = json.loads(status_file.read_text(encoding="utf-8"))
1074
+ assert runtime.get("retry_count") == 0
1075
+ assert runtime.get("infra_error_count") == 0
1076
+ assert "continuation_pending" not in runtime
1077
+
1078
+
1079
+ def test_python_context_overflow_runner_path_does_not_import_reset():
1080
+ runner_text = (PIPELINE_ROOT / "prizmkit_runtime" / "runners.py").read_text(encoding="utf-8")
1081
+
1082
+ assert "from .reset" not in runner_text
1083
+ assert "run_reset_command" not in runner_text
1084
+ assert '"context_overflow"' in runner_text
1085
+ assert "update_item" in runner_text
1086
+
1087
+
1088
+ def test_python_daemon_stale_pid_matches_snapshot(tmp_path):
1089
+ from prizmkit_runtime.daemon import run_daemon_command
1090
+
1091
+ snapshot = json.loads((REPO_ROOT / "tests" / "fixtures" / "dev-pipeline-behavior-snapshots" / "unix-runtime.json").read_text(encoding="utf-8"))
1092
+ for kind in ("feature", "bugfix", "refactor"):
1093
+ paths = _make_paths(tmp_path / f"daemon-{kind}")
1094
+ family_state = {"feature": paths.feature_state_dir, "bugfix": paths.bugfix_state_dir, "refactor": paths.refactor_state_dir}[kind]
1095
+ family_state.mkdir(parents=True, exist_ok=True)
1096
+ pid_file = family_state / ".pipeline.pid"
1097
+ pid_file.write_text("999999\n", encoding="utf-8")
1098
+ result = run_daemon_command(kind, "status", (), paths)
1099
+ expected = snapshot["daemonBehavior"]["stalePidStatusByTaskType"][kind]
1100
+
1101
+ assert result.exit_code == expected["exitCode"]
1102
+ assert json.loads(result.render())["running"] is False
1103
+ assert pid_file.exists() is False
1104
+ assert "Cleaned stale PID file" in (result.stderr or "")
1105
+
1106
+
1107
+ def test_python_daemon_controlled_start_status_stop_matches_snapshot(tmp_path):
1108
+ from prizmkit_runtime.daemon import run_daemon_command
1109
+ from prizmkit_runtime.paths import resolve_runtime_paths
1110
+
1111
+ snapshot = json.loads((REPO_ROOT / "tests" / "fixtures" / "dev-pipeline-behavior-snapshots" / "unix-runtime.json").read_text(encoding="utf-8"))["daemonBehavior"]["controlledStartStop"]
1112
+ project = tmp_path / "project"
1113
+ pipeline = tmp_path / "runtime"
1114
+ pipeline.mkdir(parents=True)
1115
+ cli = pipeline / "cli.py"
1116
+ cli.write_text(
1117
+ "#!/usr/bin/env python3\n"
1118
+ "import os, sys, time\n"
1119
+ "print('payload env=' + os.environ.get('FOO', ''))\n"
1120
+ "print('payload args=' + ' '.join(sys.argv[1:]))\n"
1121
+ "sys.stdout.flush()\n"
1122
+ "time.sleep(30)\n",
1123
+ encoding="utf-8",
1124
+ )
1125
+ cli.chmod(0o755)
1126
+ paths = resolve_runtime_paths(pipeline_root=pipeline, project_root=project)
1127
+ paths.plans_dir.mkdir(parents=True)
1128
+ paths.feature_plan.write_text(json.dumps({"features": [{"id": "F-001", "title": "A", "status": "pending"}]}), encoding="utf-8")
1129
+
1130
+ start = run_daemon_command("feature", "start", ("--env", "FOO=bar", "--features", "F-001"), paths)
1131
+ try:
1132
+ start_payload = json.loads(start.render())
1133
+ status = run_daemon_command("feature", "status", (), paths)
1134
+ status_payload = json.loads(status.render())
1135
+ finally:
1136
+ stop = run_daemon_command("feature", "stop", (), paths)
1137
+
1138
+ assert start.exit_code == snapshot["startExitCode"]
1139
+ assert start_payload["success"] is snapshot["startSuccess"]
1140
+ assert isinstance(start_payload["pid"], int)
1141
+ assert (paths.feature_state_dir / ".pipeline.pid").exists() is snapshot["pidFileExistsAfterStop"]
1142
+ assert status.exit_code == snapshot["statusWhileRunningExitCode"]
1143
+ assert status_payload["running"] is snapshot["statusWhileRunningReportsRunning"]
1144
+ assert isinstance(status_payload["pid"], int)
1145
+ assert status_payload["log_file"].endswith("pipeline-daemon.log")
1146
+ assert stop.exit_code == snapshot["stopExitCode"]
1147
+ assert json.loads(stop.render())["message"] in {"stopped", "already exited", "not running"}
1148
+ log_text = (paths.feature_state_dir / "pipeline-daemon.log").read_text(encoding="utf-8")
1149
+ assert "FOO=bar" in log_text or "payload env=bar" in log_text
1150
+ assert "--project-root" in log_text
1151
+
1152
+
1153
+ def test_python_reset_stalled_filter_selects_stalled_context_items(tmp_path):
1154
+ from prizmkit_runtime.reset import run_reset_command
1155
+
1156
+ status_by_kind = {
1157
+ "feature": ("features", "F-001"),
1158
+ "bugfix": ("bugs", "B-001"),
1159
+ "refactor": ("refactors", "R-001"),
1160
+ }
1161
+ for kind, (_collection, item_id) in status_by_kind.items():
1162
+ paths = _make_paths(tmp_path / f"stalled-{kind}")
1163
+ plan, _state_dir, status_file = _write_family_case(paths, kind, item_id)
1164
+ data = json.loads(plan.read_text(encoding="utf-8"))
1165
+ data[{"feature": "features", "bugfix": "bugs", "refactor": "refactors"}[kind]][0]["status"] = "stalled_context_continuation"
1166
+ plan.write_text(json.dumps(data), encoding="utf-8")
1167
+
1168
+ result = run_reset_command(kind, ("--stalled",), paths)
1169
+
1170
+ assert result.exit_code == 0
1171
+ assert f"Reset: {item_id}" in result.render()
1172
+ runtime = json.loads(status_file.read_text(encoding="utf-8"))
1173
+ assert runtime.get("retry_count") == 0
1174
+ assert "continuation_pending" not in runtime
1175
+ updated = json.loads(plan.read_text(encoding="utf-8"))
1176
+ assert updated[{"feature": "features", "bugfix": "bugs", "refactor": "refactors"}[kind]][0]["status"] == "pending"
1177
+
1178
+
1179
+ def test_python_daemon_progress_uses_list_status_without_runtime_files(tmp_path):
1180
+ from prizmkit_runtime.daemon import _progress_summary
1181
+ from prizmkit_runtime.runner_models import family_for
1182
+
1183
+ for kind in ("feature", "bugfix", "refactor"):
1184
+ paths = _make_paths(tmp_path / f"progress-{kind}")
1185
+ if kind == "feature":
1186
+ paths.feature_plan.write_text(
1187
+ json.dumps({"features": [
1188
+ {"id": "F-001", "title": "Done", "status": "completed"},
1189
+ {"id": "F-002", "title": "Failed", "status": "failed"},
1190
+ {"id": "F-003", "title": "Skipped", "status": "auto_skipped"},
1191
+ ]}),
1192
+ encoding="utf-8",
1193
+ )
1194
+ plan = paths.feature_plan
1195
+ elif kind == "bugfix":
1196
+ paths.bugfix_plan.write_text(
1197
+ json.dumps({"bugs": [
1198
+ {"id": "B-001", "title": "Done", "status": "completed"},
1199
+ {"id": "B-002", "title": "Failed", "status": "failed"},
1200
+ {"id": "B-003", "title": "Needs Info", "status": "needs_info"},
1201
+ ]}),
1202
+ encoding="utf-8",
1203
+ )
1204
+ plan = paths.bugfix_plan
1205
+ else:
1206
+ paths.refactor_plan.write_text(
1207
+ json.dumps({"refactors": [
1208
+ {"id": "R-001", "title": "Done", "status": "completed"},
1209
+ {"id": "R-002", "title": "Failed", "status": "failed"},
1210
+ {"id": "R-003", "title": "Skipped", "status": "auto_skipped"},
1211
+ ]}),
1212
+ encoding="utf-8",
1213
+ )
1214
+ plan = paths.refactor_plan
1215
+
1216
+ summary = _progress_summary(family_for(kind, paths), plan)
1217
+
1218
+ assert summary["total"] == 3
1219
+ assert summary["completed"] == 1
1220
+ assert summary["failed"] == 1
1221
+ assert summary["percent"] == 33.3
1222
+ if kind == "bugfix":
1223
+ assert summary["needs_info"] == 1
1224
+ else:
1225
+ assert summary["auto_skipped"] == 1
1226
+