prizmkit 1.1.99 → 1.1.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/bin/create-prizmkit.js +4 -4
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/dev-pipeline/.env.example +3 -3
  4. package/bundled/dev-pipeline/README.md +110 -90
  5. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
  6. package/bundled/dev-pipeline/cli.py +27 -0
  7. package/bundled/dev-pipeline/prizmkit_runtime/__init__.py +13 -0
  8. package/bundled/dev-pipeline/prizmkit_runtime/__main__.py +9 -0
  9. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +193 -0
  10. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +313 -0
  11. package/bundled/dev-pipeline/prizmkit_runtime/compat.py +32 -0
  12. package/bundled/dev-pipeline/prizmkit_runtime/config.py +410 -0
  13. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +478 -0
  14. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +734 -0
  15. package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +256 -0
  16. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +109 -0
  17. package/bundled/dev-pipeline/prizmkit_runtime/paths.py +110 -0
  18. package/bundled/dev-pipeline/prizmkit_runtime/processes.py +373 -0
  19. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +389 -0
  20. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +51 -0
  21. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +179 -0
  22. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +345 -0
  23. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +118 -0
  24. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +187 -0
  25. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +147 -0
  26. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +422 -0
  27. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +552 -0
  28. package/bundled/dev-pipeline/prizmkit_runtime/status.py +71 -0
  29. package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
  30. package/bundled/dev-pipeline/scripts/continuation.py +374 -0
  31. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +64 -52
  32. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +35 -25
  33. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +2 -2
  34. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
  35. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +185 -16
  36. package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
  37. package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
  38. package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
  39. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +8 -2
  40. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +4 -0
  41. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -5
  42. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +16 -6
  43. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +16 -6
  44. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  45. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  46. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -0
  47. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +13 -69
  48. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +15 -7
  49. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +42 -29
  50. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +41 -39
  51. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +4 -0
  52. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +4 -0
  53. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  54. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  55. package/bundled/dev-pipeline/tests/test_auto_skip.py +932 -0
  56. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
  57. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +100 -0
  58. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
  59. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +1138 -0
  60. package/bundled/dev-pipeline/tests/test_unified_cli.py +1045 -0
  61. package/bundled/skills/_metadata.json +1 -1
  62. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +29 -34
  63. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +6 -5
  64. package/bundled/skills/feature-pipeline-launcher/SKILL.md +29 -35
  65. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +7 -6
  66. package/bundled/skills/feature-workflow/SKILL.md +3 -6
  67. package/bundled/skills/recovery-workflow/SKILL.md +23 -32
  68. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +29 -35
  69. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +7 -6
  70. package/bundled/skills/refactor-workflow/SKILL.md +3 -6
  71. package/bundled/templates/hooks/commit-intent.json +2 -2
  72. package/bundled/templates/hooks/diff-prizm-docs.py +226 -0
  73. package/bundled/templates/hooks/prizm-pre-commit.py +45 -0
  74. package/bundled/templates/hooks/validate-prizm-docs.py +240 -0
  75. package/package.json +1 -1
  76. package/src/config.js +33 -12
  77. package/src/index.js +3 -6
  78. package/src/manifest.js +3 -2
  79. package/src/metadata.js +7 -18
  80. package/src/prompts.js +0 -12
  81. package/src/runtimes.js +18 -11
  82. package/src/scaffold.js +133 -116
  83. package/src/upgrade.js +1 -1
  84. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -500
  85. package/bundled/dev-pipeline/launch-feature-daemon.sh +0 -657
  86. package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -502
  87. package/bundled/dev-pipeline/lib/branch.sh +0 -326
  88. package/bundled/dev-pipeline/lib/common.sh +0 -1257
  89. package/bundled/dev-pipeline/lib/heartbeat.sh +0 -469
  90. package/bundled/dev-pipeline/reset-bug.sh +0 -395
  91. package/bundled/dev-pipeline/reset-feature.sh +0 -406
  92. package/bundled/dev-pipeline/reset-refactor.sh +0 -393
  93. package/bundled/dev-pipeline/run-bugfix.sh +0 -1315
  94. package/bundled/dev-pipeline/run-feature.sh +0 -1602
  95. package/bundled/dev-pipeline/run-recovery.sh +0 -648
  96. package/bundled/dev-pipeline/run-refactor.sh +0 -1348
  97. package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
  98. package/bundled/dev-pipeline/tests/test-path-resolution.sh +0 -100
  99. package/bundled/dev-pipeline-windows/.env.example +0 -36
  100. package/bundled/dev-pipeline-windows/README.md +0 -30
  101. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -533
  102. package/bundled/dev-pipeline-windows/assets/feature-list-example.json +0 -146
  103. package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -137
  104. package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +0 -9
  105. package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +0 -9
  106. package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +0 -9
  107. package/bundled/dev-pipeline-windows/lib/branch.ps1 +0 -235
  108. package/bundled/dev-pipeline-windows/lib/common.ps1 +0 -829
  109. package/bundled/dev-pipeline-windows/lib/daemon.ps1 +0 -140
  110. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +0 -967
  111. package/bundled/dev-pipeline-windows/lib/reset.ps1 +0 -87
  112. package/bundled/dev-pipeline-windows/reset-bug.ps1 +0 -9
  113. package/bundled/dev-pipeline-windows/reset-feature.ps1 +0 -9
  114. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +0 -9
  115. package/bundled/dev-pipeline-windows/run-bugfix.ps1 +0 -9
  116. package/bundled/dev-pipeline-windows/run-feature.ps1 +0 -9
  117. package/bundled/dev-pipeline-windows/run-recovery.ps1 +0 -179
  118. package/bundled/dev-pipeline-windows/run-refactor.ps1 +0 -9
  119. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +0 -228
  120. package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +0 -530
  121. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +0 -2282
  122. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +0 -767
  123. package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +0 -805
  124. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +0 -845
  125. package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +0 -316
  126. package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +0 -134
  127. package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +0 -382
  128. package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +0 -399
  129. package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +0 -102
  130. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +0 -1232
  131. package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +0 -191
  132. package/bundled/dev-pipeline-windows/scripts/prizmkit-test-gate.py +0 -446
  133. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +0 -913
  134. package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +0 -173
  135. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +0 -1558
  136. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +0 -1119
  137. package/bundled/dev-pipeline-windows/scripts/utils.py +0 -542
  138. package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +0 -7
  139. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +0 -7
  140. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +0 -65
  141. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +0 -5
  142. package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +0 -7
  143. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +0 -73
  144. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +0 -589
  145. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +0 -710
  146. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +0 -791
  147. package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +0 -263
  148. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +0 -326
  149. package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +0 -237
  150. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +0 -340
  151. package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +0 -270
  152. package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +0 -7
  153. package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +0 -91
  154. package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +0 -34
  155. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +0 -10
  156. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +0 -12
  157. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +0 -7
  158. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +0 -8
  159. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +0 -9
  160. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +0 -6
  161. package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +0 -21
  162. package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +0 -21
  163. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +0 -78
  164. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +0 -283
  165. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +0 -112
  166. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +0 -168
  167. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +0 -82
  168. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +0 -75
  169. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +0 -23
  170. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +0 -14
  171. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +0 -19
  172. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +0 -63
  173. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +0 -42
  174. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +0 -44
  175. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +0 -59
  176. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +0 -39
  177. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +0 -27
  178. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +0 -27
  179. package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +0 -39
  180. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +0 -35
  181. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +0 -35
  182. package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +0 -73
  183. package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +0 -13
  184. package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +0 -21
  185. package/bundled/dev-pipeline-windows/templates/sections/session-context.md +0 -5
  186. package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +0 -6
  187. package/bundled/dev-pipeline-windows/templates/sections/task-contract.md +0 -34
  188. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +0 -48
  189. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +0 -48
  190. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +0 -83
  191. package/bundled/skills-windows/app-planner/SKILL.md +0 -296
  192. package/bundled/skills-windows/app-planner/assets/app-design-guide.md +0 -101
  193. package/bundled/skills-windows/app-planner/references/architecture-decisions.md +0 -52
  194. package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +0 -101
  195. package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +0 -71
  196. package/bundled/skills-windows/app-planner/references/infrastructure-convention-discovery.md +0 -108
  197. package/bundled/skills-windows/app-planner/references/project-brief-guide.md +0 -82
  198. package/bundled/skills-windows/app-planner/references/project-conventions-discovery.md +0 -59
  199. package/bundled/skills-windows/app-planner/references/project-state-detection.md +0 -90
  200. package/bundled/skills-windows/app-planner/references/red-team-checklist.md +0 -40
  201. package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +0 -619
  202. package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +0 -285
  203. package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +0 -249
  204. package/bundled/skills-windows/app-planner/references/rules/backend/question-manifest.json +0 -46
  205. package/bundled/skills-windows/app-planner/references/rules/backend/template.md +0 -173
  206. package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +0 -382
  207. package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +0 -211
  208. package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +0 -184
  209. package/bundled/skills-windows/app-planner/references/rules/database/question-manifest.json +0 -39
  210. package/bundled/skills-windows/app-planner/references/rules/database/template.md +0 -158
  211. package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +0 -820
  212. package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +0 -188
  213. package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +0 -319
  214. package/bundled/skills-windows/app-planner/references/rules/frontend/question-manifest.json +0 -51
  215. package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +0 -339
  216. package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +0 -649
  217. package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +0 -290
  218. package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +0 -232
  219. package/bundled/skills-windows/app-planner/references/rules/mobile/question-manifest.json +0 -47
  220. package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +0 -175
  221. package/bundled/skills-windows/app-planner/references/rules-configuration.md +0 -46
  222. package/bundled/skills-windows/bug-fix-workflow/SKILL.md +0 -379
  223. package/bundled/skills-windows/bug-fix-workflow/references/bug-diagnosis.md +0 -41
  224. package/bundled/skills-windows/bug-planner/SKILL.md +0 -402
  225. package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +0 -43
  226. package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +0 -44
  227. package/bundled/skills-windows/bug-planner/references/error-recovery.md +0 -73
  228. package/bundled/skills-windows/bug-planner/references/input-formats.md +0 -53
  229. package/bundled/skills-windows/bug-planner/references/schema-validation.md +0 -25
  230. package/bundled/skills-windows/bug-planner/references/severity-rules.md +0 -16
  231. package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +0 -322
  232. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +0 -304
  233. package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +0 -84
  234. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +0 -381
  235. package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +0 -67
  236. package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
  237. package/bundled/skills-windows/feature-planner/SKILL.md +0 -397
  238. package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +0 -64
  239. package/bundled/skills-windows/feature-planner/assets/planning-guide.md +0 -214
  240. package/bundled/skills-windows/feature-planner/references/browser-interaction.md +0 -59
  241. package/bundled/skills-windows/feature-planner/references/completeness-review.md +0 -57
  242. package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +0 -75
  243. package/bundled/skills-windows/feature-planner/references/error-recovery.md +0 -90
  244. package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +0 -112
  245. package/bundled/skills-windows/feature-planner/references/new-project-planning.md +0 -85
  246. package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +0 -1020
  247. package/bundled/skills-windows/feature-workflow/SKILL.md +0 -374
  248. package/bundled/skills-windows/feature-workflow/references/brainstorm-guide.md +0 -137
  249. package/bundled/skills-windows/prizm-kit/SKILL.md +0 -93
  250. package/bundled/skills-windows/prizmkit-code-review/SKILL.md +0 -161
  251. package/bundled/skills-windows/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
  252. package/bundled/skills-windows/prizmkit-code-review/references/review-report-template.md +0 -31
  253. package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +0 -66
  254. package/bundled/skills-windows/prizmkit-code-review/scripts/check_loop.py +0 -186
  255. package/bundled/skills-windows/prizmkit-committer/SKILL.md +0 -89
  256. package/bundled/skills-windows/prizmkit-deploy/SKILL.md +0 -444
  257. package/bundled/skills-windows/prizmkit-deploy/references/ci-cd-workflows.md +0 -115
  258. package/bundled/skills-windows/prizmkit-deploy/references/cloud-platform-deploy.md +0 -93
  259. package/bundled/skills-windows/prizmkit-deploy/references/data-safety-examples.md +0 -120
  260. package/bundled/skills-windows/prizmkit-deploy/references/database-setup.md +0 -46
  261. package/bundled/skills-windows/prizmkit-deploy/references/deploy-config-schema.md +0 -148
  262. package/bundled/skills-windows/prizmkit-deploy/references/deploy-history-schema.md +0 -62
  263. package/bundled/skills-windows/prizmkit-deploy/references/deployment-modes.md +0 -50
  264. package/bundled/skills-windows/prizmkit-deploy/references/direct-upload.md +0 -26
  265. package/bundled/skills-windows/prizmkit-deploy/references/dns-setup.md +0 -42
  266. package/bundled/skills-windows/prizmkit-deploy/references/docker-deploy.md +0 -31
  267. package/bundled/skills-windows/prizmkit-deploy/references/firewall-setup.md +0 -37
  268. package/bundled/skills-windows/prizmkit-deploy/references/live-validation-notes.md +0 -21
  269. package/bundled/skills-windows/prizmkit-deploy/references/nginx-blue-green.md +0 -59
  270. package/bundled/skills-windows/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
  271. package/bundled/skills-windows/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
  272. package/bundled/skills-windows/prizmkit-deploy/references/ssh-takeover.md +0 -20
  273. package/bundled/skills-windows/prizmkit-deploy/references/ssl-setup.md +0 -56
  274. package/bundled/skills-windows/prizmkit-implement/SKILL.md +0 -71
  275. package/bundled/skills-windows/prizmkit-init/SKILL.md +0 -356
  276. package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +0 -82
  277. package/bundled/skills-windows/prizmkit-init/references/config-schema.md +0 -70
  278. package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +0 -41
  279. package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +0 -13
  280. package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +0 -9
  281. package/bundled/skills-windows/prizmkit-plan/SKILL.md +0 -102
  282. package/bundled/skills-windows/prizmkit-plan/assets/plan-template.md +0 -115
  283. package/bundled/skills-windows/prizmkit-plan/assets/spec-template.md +0 -73
  284. package/bundled/skills-windows/prizmkit-plan/references/clarify-guide.md +0 -67
  285. package/bundled/skills-windows/prizmkit-plan/references/examples.md +0 -85
  286. package/bundled/skills-windows/prizmkit-plan/references/verification-checklist.md +0 -60
  287. package/bundled/skills-windows/prizmkit-prizm-docs/SKILL.md +0 -129
  288. package/bundled/skills-windows/prizmkit-prizm-docs/assets/prizm-docs-format.md +0 -613
  289. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-init.md +0 -45
  290. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-rebuild.md +0 -15
  291. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-status.md +0 -14
  292. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-update.md +0 -19
  293. package/bundled/skills-windows/prizmkit-prizm-docs/references/op-validate.md +0 -17
  294. package/bundled/skills-windows/prizmkit-retrospective/SKILL.md +0 -87
  295. package/bundled/skills-windows/prizmkit-retrospective/references/knowledge-injection-steps.md +0 -50
  296. package/bundled/skills-windows/prizmkit-retrospective/references/structural-sync-steps.md +0 -43
  297. package/bundled/skills-windows/prizmkit-test/SKILL.md +0 -227
  298. package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +0 -220
  299. package/bundled/skills-windows/prizmkit-test/references/examples.md +0 -143
  300. package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +0 -225
  301. package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +0 -109
  302. package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +0 -111
  303. package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +0 -347
  304. package/bundled/skills-windows/recovery-workflow/SKILL.md +0 -355
  305. package/bundled/skills-windows/recovery-workflow/evals/evals.json +0 -46
  306. package/bundled/skills-windows/recovery-workflow/references/detection.md +0 -58
  307. package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +0 -544
  308. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +0 -346
  309. package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +0 -72
  310. package/bundled/skills-windows/refactor-planner/SKILL.md +0 -398
  311. package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +0 -292
  312. package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +0 -301
  313. package/bundled/skills-windows/refactor-planner/references/fast-path.md +0 -59
  314. package/bundled/skills-windows/refactor-planner/references/planning-phases.md +0 -135
  315. package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +0 -221
  316. package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +0 -858
  317. package/bundled/skills-windows/refactor-workflow/SKILL.md +0 -343
  318. package/bundled/skills-windows/refactor-workflow/references/brainstorm-guide.md +0 -116
  319. package/bundled/templates/hooks/diff-prizm-docs.sh +0 -158
  320. package/bundled/templates/hooks/prizm-pre-commit.sh +0 -108
  321. package/bundled/templates/hooks/validate-prizm-docs.sh +0 -139
@@ -1,1558 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Core state machine for updating feature status in the dev-pipeline.
3
-
4
- Handles nine actions:
5
- - get_next: Find the next feature to process based on priority and dependencies
6
- - start: Mark a feature as in_progress when a session starts
7
- - update: Update a feature's status based on session outcome
8
- - status: Print a formatted overview of all features
9
- - pause: Save pipeline state for graceful shutdown
10
- - reset: Reset a feature to pending (status + retry count)
11
- - clean: Reset + delete session history + delete prizmkit artifacts
12
- - complete: Shortcut for manually marking a feature as completed
13
- - unskip: Recover auto-skipped features (reset failed/skipped upstream + auto_skipped downstream)
14
-
15
- Usage:
16
- python3 update-feature-status.py \
17
- --feature-list <path> --state-dir <path> \
18
- --action <get_next|start|update|status|pause|reset|clean|complete|unskip> \
19
- [--feature-id <id>] [--session-status <status>] \
20
- [--session-id <id>] [--max-retries <n>] [--max-infra-retries <n>] \
21
- [--features <filter>]
22
- """
23
-
24
- import argparse
25
- import json
26
- import os
27
- import re
28
- import shutil
29
- import sys
30
- from datetime import datetime, timezone
31
-
32
- from utils import (
33
- load_json_file,
34
- write_json_file,
35
- error_out,
36
- pad_right,
37
- _build_progress_bar,
38
- )
39
-
40
-
41
- SESSION_STATUS_VALUES = [
42
- "success",
43
- "partial_resumable",
44
- "partial_not_resumable",
45
- "failed",
46
- "crashed",
47
- "timed_out",
48
- "infra_error",
49
- "commit_missing",
50
- "docs_missing",
51
- "merge_conflict",
52
- ]
53
-
54
- TERMINAL_STATUSES = {"completed", "failed", "skipped", "auto_skipped", "split"}
55
-
56
-
57
- def parse_args():
58
- parser = argparse.ArgumentParser(
59
- description="Core state machine for dev-pipeline feature status management."
60
- )
61
- parser.add_argument(
62
- "--feature-list",
63
- required=True,
64
- help="Path to the .prizmkit/plans/feature-list.json file",
65
- )
66
- parser.add_argument(
67
- "--state-dir",
68
- required=True,
69
- help="Path to the state directory (default: .prizmkit/state/features)",
70
- )
71
- parser.add_argument(
72
- "--action",
73
- required=True,
74
- choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "complete", "unskip"],
75
- help="Action to perform",
76
- )
77
- parser.add_argument(
78
- "--feature-id",
79
- default=None,
80
- help="Feature ID (required for start/reset/clean/complete/update actions)",
81
- )
82
- parser.add_argument(
83
- "--session-status",
84
- default=None,
85
- choices=SESSION_STATUS_VALUES,
86
- help="Session outcome status (required for 'update' action)",
87
- )
88
- parser.add_argument(
89
- "--session-id",
90
- default=None,
91
- help="Session ID (optional, for 'update' action)",
92
- )
93
- parser.add_argument(
94
- "--max-retries",
95
- type=int,
96
- default=3,
97
- help="Maximum code retry count before marking as failed (default: 3)",
98
- )
99
- parser.add_argument(
100
- "--max-infra-retries",
101
- type=int,
102
- default=3,
103
- help="Maximum infrastructure retry count before marking as failed (default: 3)",
104
- )
105
- parser.add_argument(
106
- "--feature-slug",
107
- default=None,
108
- help="Feature slug (e.g. 007-import-export-desktop). Required for 'clean' action.",
109
- )
110
- parser.add_argument(
111
- "--project-root",
112
- default=None,
113
- help="Project root directory. Required for 'clean' action.",
114
- )
115
- parser.add_argument(
116
- "--features",
117
- default=None,
118
- help="Feature filter: comma-separated IDs (F-001,F-003) or range (F-001:F-010), or mixed.",
119
- )
120
- return parser.parse_args()
121
-
122
-
123
- def parse_feature_filter(features_str):
124
- """Parse --features argument into a set of feature IDs.
125
-
126
- Supported formats:
127
- F-001,F-003,F-005 -> {"F-001", "F-003", "F-005"}
128
- F-001:F-010 -> {"F-001", "F-002", ..., "F-010"}
129
- F-001,F-005:F-010 -> mixed, union of both
130
-
131
- Returns None if features_str is None/empty (meaning no filter).
132
- """
133
- if not features_str:
134
- return None
135
-
136
- result = set()
137
- for part in features_str.split(","):
138
- part = part.strip()
139
- if not part:
140
- continue
141
- if ":" in part:
142
- tokens = part.split(":", 1)
143
- m_start = re.search(r"\d+", tokens[0])
144
- m_end = re.search(r"\d+", tokens[1])
145
- if not m_start or not m_end:
146
- error_out("Invalid range format: {}".format(part))
147
- start_num = int(m_start.group())
148
- end_num = int(m_end.group())
149
- if start_num > end_num:
150
- start_num, end_num = end_num, start_num
151
- for i in range(start_num, end_num + 1):
152
- result.add("F-{:03d}".format(i))
153
- else:
154
- result.add(part.upper())
155
- return result if result else None
156
-
157
-
158
- def now_iso():
159
- """Return the current UTC time in ISO8601 format."""
160
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
161
-
162
-
163
- def load_feature_status(state_dir, feature_id):
164
- """Load the runtime state from status.json for a feature.
165
-
166
- Returns runtime fields only (retry_count, sessions, etc.).
167
- The 'status' field is NOT included — status lives exclusively
168
- in feature-list.json.
169
- """
170
- status_path = os.path.join(state_dir, feature_id, "status.json")
171
- if not os.path.isfile(status_path):
172
- now = now_iso()
173
- return {
174
- "feature_id": feature_id,
175
- "retry_count": 0,
176
- "max_retries": 3,
177
- "max_infra_retries": 3,
178
- "infra_error_count": 0,
179
- "last_infra_error_session_id": None,
180
- "sessions": [],
181
- "last_session_id": None,
182
- "resume_from_phase": None,
183
- "created_at": now,
184
- "updated_at": now,
185
- }
186
- data, err = load_json_file(status_path)
187
- if err:
188
- now = now_iso()
189
- return {
190
- "feature_id": feature_id,
191
- "retry_count": 0,
192
- "max_retries": 3,
193
- "max_infra_retries": 3,
194
- "infra_error_count": 0,
195
- "last_infra_error_session_id": None,
196
- "sessions": [],
197
- "last_session_id": None,
198
- "resume_from_phase": None,
199
- "created_at": now,
200
- "updated_at": now,
201
- }
202
- # Defensively remove status if present (legacy data)
203
- data.pop("status", None)
204
- return data
205
-
206
-
207
- def save_feature_status(state_dir, feature_id, status_data):
208
- """Write the status.json for a feature (runtime fields only)."""
209
- # Defensively strip status — it belongs in feature-list.json
210
- status_data.pop("status", None)
211
- status_path = os.path.join(state_dir, feature_id, "status.json")
212
- return write_json_file(status_path, status_data)
213
-
214
-
215
- def get_feature_status_from_list(feature_list_path, feature_id):
216
- """Read a single feature's status from feature-list.json."""
217
- data, err = load_json_file(feature_list_path)
218
- if err:
219
- return "pending"
220
- for f in data.get("features", []):
221
- if isinstance(f, dict) and f.get("id") == feature_id:
222
- return f.get("status", "pending")
223
- return "pending"
224
-
225
-
226
- def update_feature_in_list(feature_list_path, feature_id, new_status):
227
- """Update a feature's status field in .prizmkit/plans/feature-list.json.
228
-
229
- Reads the whole file, modifies the target feature's status, writes back.
230
- Returns an error string on failure, None on success.
231
- """
232
- data, err = load_json_file(feature_list_path)
233
- if err:
234
- return err
235
- features = data.get("features", [])
236
- found = False
237
- for feature in features:
238
- if isinstance(feature, dict) and feature.get("id") == feature_id:
239
- feature["status"] = new_status
240
- found = True
241
- break
242
- if not found:
243
- return "Feature '{}' not found in .prizmkit/plans/feature-list.json".format(feature_id)
244
- return write_json_file(feature_list_path, data)
245
-
246
-
247
- def _default_project_root():
248
- # Script lives at <pipeline>/scripts/update-feature-status.py.
249
- # Pipeline may be either <project>/.prizmkit/dev-pipeline (user install)
250
- # or <repo>/dev-pipeline (framework source). Auto-detect by checking if
251
- # the parent of dev-pipeline is named ".prizmkit".
252
- env = os.environ.get("PROJECT_ROOT")
253
- if env:
254
- return os.path.abspath(env)
255
- script_dir = os.path.dirname(os.path.abspath(__file__))
256
- pipeline_dir = os.path.dirname(script_dir)
257
- pipeline_parent = os.path.dirname(pipeline_dir)
258
- if os.path.basename(pipeline_parent) == ".prizmkit":
259
- return os.path.dirname(pipeline_parent)
260
- return pipeline_parent
261
-
262
-
263
- def _build_feature_slug(feature_id, title):
264
- numeric = feature_id.replace("F-", "").replace("f-", "").zfill(3)
265
- cleaned = re.sub(r"[^a-z0-9\s-]", "", (title or "").lower())
266
- cleaned = re.sub(r"[\s]+", "-", cleaned.strip())
267
- cleaned = re.sub(r"-+", "-", cleaned).strip("-")
268
- if not cleaned:
269
- cleaned = "feature"
270
- return "{}-{}".format(numeric, cleaned)
271
-
272
-
273
- def _get_feature_slug(feature_list_path, feature_id):
274
- data, err = load_json_file(feature_list_path)
275
- if err:
276
- return None
277
- for feature in data.get("features", []):
278
- if isinstance(feature, dict) and feature.get("id") == feature_id:
279
- return _build_feature_slug(feature_id, feature.get("title", ""))
280
- return None
281
-
282
-
283
- def cleanup_feature_artifacts(feature_list_path, state_dir, feature_id, project_root=None):
284
- """Delete intermediate artifacts for a failed feature run.
285
-
286
- Cleans session history, per-feature transient state, generated specs,
287
- current-session pointer, and .dev-team workspace to avoid context pollution.
288
- """
289
- if not project_root:
290
- project_root = _default_project_root()
291
-
292
- cleaned = []
293
-
294
- # 1) Remove all session history
295
- sessions_dir = os.path.join(state_dir, feature_id, "sessions")
296
- sessions_deleted = 0
297
- if os.path.isdir(sessions_dir):
298
- for entry in os.listdir(sessions_dir):
299
- entry_path = os.path.join(sessions_dir, entry)
300
- if os.path.isdir(entry_path):
301
- shutil.rmtree(entry_path)
302
- sessions_deleted += 1
303
- cleaned.append("Deleted {} session(s) from {}".format(sessions_deleted, sessions_dir))
304
-
305
- # 2) Remove transient files under feature state dir (keep status.json)
306
- feature_dir = os.path.join(state_dir, feature_id)
307
- if os.path.isdir(feature_dir):
308
- for entry in os.listdir(feature_dir):
309
- if entry == "status.json" or entry == "sessions":
310
- continue
311
- entry_path = os.path.join(feature_dir, entry)
312
- if os.path.isdir(entry_path):
313
- shutil.rmtree(entry_path)
314
- cleaned.append("Deleted directory {}".format(entry_path))
315
- elif os.path.isfile(entry_path):
316
- os.remove(entry_path)
317
- cleaned.append("Deleted file {}".format(entry_path))
318
-
319
- # 3) Remove generated prizm specs for this feature
320
- feature_slug = _get_feature_slug(feature_list_path, feature_id)
321
- if feature_slug:
322
- specs_dir = os.path.join(project_root, ".prizmkit", "specs", feature_slug)
323
- if os.path.isdir(specs_dir):
324
- file_count = sum(len(files) for _, _, files in os.walk(specs_dir))
325
- shutil.rmtree(specs_dir)
326
- cleaned.append("Deleted {} ({} files)".format(specs_dir, file_count))
327
-
328
- # 4) Remove global dev-team workspace to avoid stale context contamination
329
- dev_team_dir = os.path.join(project_root, ".dev-team")
330
- if os.path.isdir(dev_team_dir):
331
- file_count = sum(len(files) for _, _, files in os.walk(dev_team_dir))
332
- shutil.rmtree(dev_team_dir)
333
- cleaned.append("Deleted {} ({} files)".format(dev_team_dir, file_count))
334
-
335
- # 5) Clear current-session pointer if it points to this feature
336
- # (no-op: current-session.json has been removed from the pipeline)
337
-
338
- return cleaned
339
-
340
-
341
- def load_session_status(state_dir, feature_id, session_id):
342
- """Load a session's session-status.json file."""
343
- session_status_path = os.path.join(state_dir, feature_id, "sessions",
344
- session_id, "session-status.json"
345
- )
346
- data, err = load_json_file(session_status_path)
347
- if err:
348
- return None, err
349
- return data, None
350
-
351
-
352
- # ---------------------------------------------------------------------------
353
- # Auto-skip: cascade failure to blocked downstream features
354
- # ---------------------------------------------------------------------------
355
-
356
- def auto_skip_blocked_features(feature_list_path, state_dir, failed_feature_id):
357
- """Recursively mark all downstream features blocked by a failed feature as auto_skipped.
358
-
359
- When a feature is marked as failed, any feature whose dependency chain includes
360
- the failed feature can never be executed. This function propagates the failure
361
- by marking those blocked features as auto_skipped, allowing the pipeline to
362
- continue processing unblocked features and eventually reach PIPELINE_COMPLETE.
363
-
364
- Re-reads .prizmkit/plans/feature-list.json from disk to get the latest state (including the
365
- just-written failed status from update_feature_in_list).
366
-
367
- NOTE: This function performs a read-modify-write on .prizmkit/plans/feature-list.json without
368
- file locking. The caller (action_update) also writes to .prizmkit/plans/feature-list.json
369
- immediately before calling this. Safe for single-pipeline execution, but if
370
- multiple pipeline instances share the same .prizmkit/plans/feature-list.json concurrently,
371
- a race condition may cause lost writes. Add file locking if parallel pipelines
372
- are introduced.
373
- """
374
- data, err = load_json_file(feature_list_path)
375
- if err:
376
- return []
377
- features = data.get("features", [])
378
-
379
- # Build current status map
380
- status_map = {}
381
- for f in features:
382
- if isinstance(f, dict) and f.get("id"):
383
- status_map[f["id"]] = f.get("status", "pending")
384
-
385
- # Collect all features to auto-skip (recursive propagation)
386
- to_skip = set()
387
- changed = True
388
- while changed:
389
- changed = False
390
- for f in features:
391
- if not isinstance(f, dict):
392
- continue
393
- fid = f.get("id")
394
- if not fid or fid in to_skip:
395
- continue
396
- current = status_map.get(fid, "pending")
397
- if current in TERMINAL_STATUSES:
398
- continue
399
- deps = f.get("dependencies", [])
400
- for dep_id in deps:
401
- dep_status = status_map.get(dep_id, "pending")
402
- if dep_status in ("failed", "skipped", "auto_skipped") or dep_id in to_skip:
403
- to_skip.add(fid)
404
- status_map[fid] = "auto_skipped"
405
- changed = True
406
- break
407
-
408
- if not to_skip:
409
- return []
410
-
411
- # Batch-write to .prizmkit/plans/feature-list.json
412
- for f in features:
413
- if isinstance(f, dict) and f.get("id") in to_skip:
414
- f["status"] = "auto_skipped"
415
- write_json_file(feature_list_path, data)
416
-
417
- # Update timestamps in status.json for each auto-skipped feature
418
- for fid in to_skip:
419
- fs = load_feature_status(state_dir, fid)
420
- fs["updated_at"] = now_iso()
421
- save_feature_status(state_dir, fid, fs)
422
-
423
- # Build blocking reason map for logging
424
- skipped_info = []
425
- for f in features:
426
- if not isinstance(f, dict):
427
- continue
428
- fid = f.get("id")
429
- if fid not in to_skip:
430
- continue
431
- deps = f.get("dependencies", [])
432
- blockers = [
433
- d for d in deps
434
- if d == failed_feature_id or d in to_skip
435
- ]
436
- skipped_info.append({
437
- "feature_id": fid,
438
- "title": f.get("title", ""),
439
- "blocked_by": blockers,
440
- })
441
-
442
- print(
443
- "[auto-skip] {} feature(s) auto-skipped due to failed {}:".format(
444
- len(skipped_info), failed_feature_id
445
- ),
446
- file=sys.stderr,
447
- )
448
- for info in skipped_info:
449
- print(
450
- " {} ({}) — blocked by {}".format(
451
- info["feature_id"],
452
- info["title"],
453
- ", ".join(info["blocked_by"]),
454
- ),
455
- file=sys.stderr,
456
- )
457
-
458
- return skipped_info
459
-
460
-
461
- # ---------------------------------------------------------------------------
462
- # Action: get_next
463
- # ---------------------------------------------------------------------------
464
-
465
- def action_get_next(feature_list_data, state_dir, feature_filter=None):
466
- """Find the next feature to process.
467
-
468
- Priority logic:
469
- 1. Skip terminal statuses (completed, failed, skipped, auto_skipped, split)
470
- 2. If feature_filter is set, skip features not in the filter
471
- 3. Check that all dependencies are completed
472
- 4. Prefer in_progress features over pending ones (interrupted session resume)
473
- 5. Among eligible features, pick highest priority (high > medium > low)
474
- """
475
- features = feature_list_data.get("features", [])
476
- if not features:
477
- print("PIPELINE_COMPLETE")
478
- return
479
-
480
- # Build status map from ALL features (for dependency checking).
481
- # Status comes from feature-list.json (the single source of truth).
482
- # This must happen BEFORE the feature filter is applied, because
483
- # filtered features may depend on features outside the filter.
484
- status_map = {} # feature_id -> status string
485
- status_data_map = {} # feature_id -> runtime status data (retry_count, etc.)
486
- for feature in features:
487
- if not isinstance(feature, dict):
488
- continue
489
- fid = feature.get("id")
490
- if not fid:
491
- continue
492
- status_map[fid] = feature.get("status", "pending")
493
- fs = load_feature_status(state_dir, fid)
494
- status_data_map[fid] = fs
495
-
496
- # Apply feature filter: only consider these features as candidates
497
- # for execution, but dependency checking still uses the full status_map
498
- if feature_filter is not None:
499
- features = [
500
- f for f in features
501
- if isinstance(f, dict) and f.get("id") in feature_filter
502
- ]
503
- if not features:
504
- print("PIPELINE_COMPLETE")
505
- return
506
-
507
- # Check if all features are in terminal state
508
- non_terminal = [
509
- f for f in features
510
- if isinstance(f, dict) and f.get("id")
511
- and status_map.get(f["id"], "pending") not in TERMINAL_STATUSES
512
- ]
513
- if not non_terminal:
514
- print("PIPELINE_COMPLETE")
515
- return
516
-
517
- # Find eligible features (dependencies all completed)
518
- eligible = []
519
- has_remaining = False
520
- for feature in non_terminal:
521
- fid = feature.get("id")
522
- if not fid:
523
- continue
524
- has_remaining = True
525
- deps = feature.get("dependencies", [])
526
- all_deps_completed = True
527
- for dep_id in deps:
528
- if status_map.get(dep_id, "pending") != "completed":
529
- all_deps_completed = False
530
- break
531
- if all_deps_completed:
532
- eligible.append(feature)
533
-
534
- if not eligible:
535
- if has_remaining:
536
- print("PIPELINE_BLOCKED")
537
- else:
538
- print("PIPELINE_COMPLETE")
539
- return
540
-
541
- # Separate in_progress from pending
542
- in_progress_features = []
543
- pending_features = []
544
- for feature in eligible:
545
- fid = feature.get("id")
546
- fstatus = status_map.get(fid, "pending")
547
- if fstatus == "in_progress":
548
- in_progress_features.append(feature)
549
- else:
550
- pending_features.append(feature)
551
-
552
- # Priority mapping: string enum → sort order (critical first)
553
- _PRIORITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
554
-
555
- # Prefer in_progress features, then pending; sort by priority (high > medium > low)
556
- if in_progress_features:
557
- candidates = sorted(
558
- in_progress_features,
559
- key=lambda f: _PRIORITY_ORDER.get(f.get("priority", "low"), 3)
560
- )
561
- else:
562
- candidates = sorted(
563
- pending_features,
564
- key=lambda f: _PRIORITY_ORDER.get(f.get("priority", "low"), 3)
565
- )
566
-
567
- chosen = candidates[0]
568
- chosen_id = chosen["id"]
569
- chosen_status_data = status_data_map.get(chosen_id, {})
570
-
571
- result = {
572
- "feature_id": chosen_id,
573
- "title": chosen.get("title", ""),
574
- "retry_count": chosen_status_data.get("retry_count", 0),
575
- "infra_error_count": chosen_status_data.get("infra_error_count", 0),
576
- "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
577
- }
578
- print(json.dumps(result, indent=2, ensure_ascii=False))
579
-
580
-
581
- # ---------------------------------------------------------------------------
582
- # Action: update
583
- # ---------------------------------------------------------------------------
584
-
585
- def action_update(args, feature_list_path, state_dir):
586
- """Update a feature's status based on session outcome.
587
-
588
- Failure policy:
589
- - Never continue from partial/failed session context
590
- - Always clean intermediate artifacts and restart from scratch
591
- """
592
- feature_id = args.feature_id
593
- session_status = args.session_status
594
- session_id = args.session_id
595
- max_retries = args.max_retries
596
- max_infra_retries = args.max_infra_retries
597
-
598
- if not feature_id:
599
- error_out("--feature-id is required for 'update' action")
600
- return
601
- if not session_status:
602
- error_out("--session-status is required for 'update' action")
603
- return
604
-
605
- fs = load_feature_status(state_dir, feature_id)
606
- current_list_status = get_feature_status_from_list(feature_list_path, feature_id)
607
-
608
- # Track what status we write to feature-list.json
609
- new_status = current_list_status
610
-
611
- if session_status == "success":
612
- fs["infra_error_count"] = 0
613
- fs["last_infra_error_session_id"] = None
614
- # No-op guard: if this exact successful session was already recorded,
615
- # avoid rewriting state files again (prevents post-commit dirty changes).
616
- existing_sessions = fs.get("sessions", [])
617
- already_completed = current_list_status == "completed" and fs.get("resume_from_phase") is None
618
- same_session_already_recorded = (
619
- session_id
620
- and session_id in existing_sessions
621
- and fs.get("last_session_id") == session_id
622
- )
623
- if already_completed and (same_session_already_recorded or not session_id):
624
- summary = {
625
- "action": "update",
626
- "feature_id": feature_id,
627
- "session_status": session_status,
628
- "new_status": "completed",
629
- "retry_count": fs.get("retry_count", 0),
630
- "infra_error_count": fs.get("infra_error_count", 0),
631
- "max_infra_retries": max_infra_retries,
632
- "resume_from_phase": fs.get("resume_from_phase"),
633
- "updated_at": fs.get("updated_at"),
634
- "no_op": True,
635
- }
636
- print(json.dumps(summary, indent=2, ensure_ascii=False))
637
- return
638
-
639
- new_status = "completed"
640
- fs["resume_from_phase"] = None
641
- err = update_feature_in_list(feature_list_path, feature_id, "completed")
642
- if err:
643
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
644
- return
645
- elif session_status in ("commit_missing", "docs_missing", "merge_conflict"):
646
- # Degraded outcome: keep artifacts for retry.
647
- # Store granular reason in status.json (internal state),
648
- # but write only schema-valid status to feature-list.json.
649
- fs["retry_count"] = fs.get("retry_count", 0) + 1
650
-
651
- if fs["retry_count"] >= max_retries:
652
- new_status = "failed"
653
- else:
654
- # feature-list.json gets schema-valid "pending" (will be retried)
655
- new_status = "pending"
656
-
657
- fs["degraded_reason"] = session_status
658
- fs["resume_from_phase"] = None
659
- fs["sessions"] = []
660
- if session_id:
661
- fs["last_session_id"] = session_id
662
- fs["last_failed_session_id"] = session_id
663
-
664
- err = update_feature_in_list(feature_list_path, feature_id, new_status)
665
- if err:
666
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
667
- return
668
- elif session_status == "infra_error":
669
- # AI CLI/provider outage, auth failure, gateway error, etc.
670
- # Infra failures do not consume the code retry budget, but they still
671
- # need their own bounded budget so a flaky provider cannot loop forever.
672
- infra_error_count = fs.get("infra_error_count", 0) + 1
673
- fs["infra_error_count"] = infra_error_count
674
- fs["last_infra_error_session_id"] = session_id
675
- fs["max_infra_retries"] = max_infra_retries
676
- fs["degraded_reason"] = "infra_error"
677
- if session_id:
678
- fs["last_session_id"] = session_id
679
- fs["resume_from_phase"] = None
680
-
681
- if infra_error_count >= max_infra_retries:
682
- new_status = "failed"
683
- if session_id:
684
- fs["last_failed_session_id"] = session_id
685
- else:
686
- new_status = "pending"
687
-
688
- err = update_feature_in_list(feature_list_path, feature_id, new_status)
689
- if err:
690
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
691
- return
692
- else:
693
- # crashed / failed / timed_out — preserve all artifacts for debugging.
694
- fs["retry_count"] = fs.get("retry_count", 0) + 1
695
-
696
- if fs["retry_count"] >= max_retries:
697
- new_status = "failed"
698
- else:
699
- new_status = "pending"
700
-
701
- fs["resume_from_phase"] = None
702
- if session_id:
703
- fs["last_session_id"] = session_id
704
- fs["last_failed_session_id"] = session_id
705
- # Keep sessions list and last_session_id for debugging
706
-
707
- err = update_feature_in_list(feature_list_path, feature_id, new_status)
708
- if err:
709
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
710
- return
711
-
712
- if session_status == "success" and session_id:
713
- sessions = fs.get("sessions", [])
714
- if session_id not in sessions:
715
- sessions.append(session_id)
716
- fs["sessions"] = sessions
717
- fs["last_session_id"] = session_id
718
-
719
- fs["updated_at"] = now_iso()
720
-
721
- err = save_feature_status(state_dir, feature_id, fs)
722
- if err:
723
- error_out("Failed to save feature status: {}".format(err))
724
- return
725
-
726
- # Auto-skip downstream features when this feature is marked as failed or skipped
727
- auto_skipped_features = []
728
- if new_status in ("failed", "skipped"):
729
- auto_skipped_features = auto_skip_blocked_features(
730
- feature_list_path, state_dir, feature_id
731
- )
732
-
733
- summary = {
734
- "action": "update",
735
- "feature_id": feature_id,
736
- "session_status": session_status,
737
- "new_status": new_status,
738
- "retry_count": fs["retry_count"],
739
- "infra_error_count": fs.get("infra_error_count", 0),
740
- "max_infra_retries": max_infra_retries,
741
- "resume_from_phase": fs.get("resume_from_phase"),
742
- "updated_at": fs["updated_at"],
743
- }
744
- if auto_skipped_features:
745
- summary["auto_skipped"] = [info["feature_id"] for info in auto_skipped_features]
746
- if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
747
- summary["degraded_reason"] = session_status
748
- summary["restart_policy"] = "finalization_retry"
749
- elif session_status == "infra_error":
750
- summary["restart_policy"] = "infra_retry"
751
- summary["infra_error_count"] = fs.get("infra_error_count", 0)
752
- summary["artifacts_preserved"] = True
753
- elif session_status != "success":
754
- summary["restart_policy"] = "preserve_and_retry"
755
- summary["artifacts_preserved"] = True
756
-
757
- print(json.dumps(summary, indent=2, ensure_ascii=False))
758
-
759
-
760
- # ---------------------------------------------------------------------------
761
- # Action: status
762
- # ---------------------------------------------------------------------------
763
-
764
- # ANSI color codes
765
- COLOR_GREEN = "\033[92m"
766
- COLOR_YELLOW = "\033[93m"
767
- COLOR_RED = "\033[91m"
768
- COLOR_GRAY = "\033[90m"
769
- COLOR_BOLD = "\033[1m"
770
- COLOR_RESET = "\033[0m"
771
-
772
- BOX_WIDTH = 68
773
-
774
-
775
- def _calc_feature_duration(state_dir, feature_id):
776
- """Calculate the duration (in seconds) of a completed feature.
777
-
778
- Computes duration from status.json's created_at and updated_at fields.
779
- If session records exist, attempts to use the first session's started_at
780
- to the last update time for the calculation.
781
- Returns None if the duration cannot be calculated.
782
- """
783
- fs_path = os.path.join(state_dir, feature_id, "status.json")
784
- if not os.path.isfile(fs_path):
785
- return None
786
- data, err = load_json_file(fs_path)
787
- if err or not data:
788
- return None
789
-
790
- created_at = data.get("created_at")
791
- updated_at = data.get("updated_at")
792
- if not created_at or not updated_at:
793
- return None
794
-
795
- try:
796
- fmt = "%Y-%m-%dT%H:%M:%SZ"
797
- t_start = datetime.strptime(created_at, fmt)
798
- t_end = datetime.strptime(updated_at, fmt)
799
- delta = (t_end - t_start).total_seconds()
800
- # Filter outliers: ignore durations less than 10s or more than 24h
801
- if delta < 10 or delta > 86400:
802
- return None
803
- return delta
804
- except (ValueError, TypeError):
805
- return None
806
-
807
-
808
- def _format_duration(seconds):
809
- """Format seconds into a human-readable duration string."""
810
- if seconds is None:
811
- return "N/A"
812
- seconds = int(seconds)
813
- if seconds < 60:
814
- return "{}s".format(seconds)
815
- elif seconds < 3600:
816
- m = seconds // 60
817
- s = seconds % 60
818
- return "{}m{}s".format(m, s)
819
- else:
820
- h = seconds // 3600
821
- m = (seconds % 3600) // 60
822
- return "{}h{}m".format(h, m)
823
-
824
-
825
- def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None):
826
- """Estimate remaining time based on completed feature durations, weighted by complexity.
827
-
828
- Strategy:
829
- 1. Collect durations of all completed features, grouped by complexity
830
- 2. For remaining pending/in_progress features, estimate using the average duration
831
- of the corresponding complexity level
832
- 3. If no historical data exists for a complexity level, fall back to the global average
833
-
834
- Returns an (estimated_seconds, confidence) tuple.
835
- confidence: "high" (>=50% completed), "medium" (>=25%), "low" (<25%)
836
- """
837
- # Complexity weights (used for estimation when no historical data is available)
838
- COMPLEXITY_WEIGHT = {"low": 1.0, "medium": 2.0, "high": 4.0}
839
-
840
- # Build feature-list status map (status lives in feature-list.json)
841
- fl_status_map = {}
842
- if feature_list_data:
843
- for f in feature_list_data.get("features", []):
844
- if isinstance(f, dict) and f.get("id"):
845
- fl_status_map[f["id"]] = f.get("status", "pending")
846
-
847
- # Collect completed feature durations grouped by complexity
848
- duration_by_complexity = {} # complexity -> [duration_seconds]
849
- feature_complexity_map = {} # feature_id -> complexity
850
-
851
- for feature in features:
852
- if not isinstance(feature, dict):
853
- continue
854
- fid = feature.get("id")
855
- if not fid:
856
- continue
857
- complexity = feature.get("estimated_complexity", "medium")
858
- feature_complexity_map[fid] = complexity
859
-
860
- all_durations = []
861
- for feature in features:
862
- if not isinstance(feature, dict):
863
- continue
864
- fid = feature.get("id")
865
- if not fid:
866
- continue
867
- if fl_status_map.get(fid) != "completed":
868
- continue
869
- duration = _calc_feature_duration(state_dir, fid)
870
- if duration is None:
871
- continue
872
- complexity = feature_complexity_map.get(fid, "medium")
873
- if complexity not in duration_by_complexity:
874
- duration_by_complexity[complexity] = []
875
- duration_by_complexity[complexity].append(duration)
876
- all_durations.append(duration)
877
-
878
- if not all_durations:
879
- return None, "low"
880
-
881
- # Calculate average duration per complexity level
882
- avg_by_complexity = {}
883
- for c, durations in duration_by_complexity.items():
884
- avg_by_complexity[c] = sum(durations) / len(durations)
885
- global_avg = sum(all_durations) / len(all_durations)
886
-
887
- # Estimate duration for remaining features
888
- remaining_seconds = 0.0
889
- remaining_count = 0
890
- for feature in features:
891
- if not isinstance(feature, dict):
892
- continue
893
- fid = feature.get("id")
894
- if not fid:
895
- continue
896
- fstatus = fl_status_map.get(fid, "pending")
897
- if fstatus in TERMINAL_STATUSES:
898
- continue
899
- remaining_count += 1
900
- complexity = feature_complexity_map.get(fid, "medium")
901
- if complexity in avg_by_complexity:
902
- remaining_seconds += avg_by_complexity[complexity]
903
- else:
904
- # No historical data for this complexity; estimate using global avg × weight ratio
905
- weight = COMPLEXITY_WEIGHT.get(complexity, 2.0)
906
- base_weight = COMPLEXITY_WEIGHT.get("medium", 2.0)
907
- remaining_seconds += global_avg * (weight / base_weight)
908
-
909
- # Calculate confidence level
910
- total = len([f for f in features if isinstance(f, dict) and f.get("id")])
911
- completed = counts.get("completed", 0)
912
- if total > 0:
913
- ratio = completed / total
914
- if ratio >= 0.5:
915
- confidence = "high"
916
- elif ratio >= 0.25:
917
- confidence = "medium"
918
- else:
919
- confidence = "low"
920
- else:
921
- confidence = "low"
922
-
923
- return remaining_seconds, confidence
924
-
925
-
926
- def action_status(feature_list_data, state_dir, feature_filter=None):
927
- """Print a formatted overview of all features and their status.
928
-
929
- Status is read exclusively from .prizmkit/plans/feature-list.json (the single source of
930
- truth). state_dir is only used for ETA estimation when session history
931
- is available.
932
- """
933
- features = feature_list_data.get("features", [])
934
- app_name = feature_list_data.get("project_name", feature_list_data.get("app_name", "Unknown"))
935
-
936
- # Apply feature filter
937
- if feature_filter is not None:
938
- features = [
939
- f for f in features
940
- if isinstance(f, dict) and f.get("id") in feature_filter
941
- ]
942
-
943
- # Gather status info
944
- counts = {
945
- "completed": 0,
946
- "in_progress": 0,
947
- "failed": 0,
948
- "pending": 0,
949
- "skipped": 0,
950
- "auto_skipped": 0,
951
- }
952
- feature_lines = []
953
-
954
- # Build status map from .prizmkit/plans/feature-list.json only
955
- status_map = {}
956
- for feature in features:
957
- if not isinstance(feature, dict):
958
- continue
959
- fid = feature.get("id")
960
- if not fid:
961
- continue
962
- status_map[fid] = feature.get("status", "pending")
963
-
964
- # Build degraded_reason map from status.json (internal pipeline state)
965
- degraded_reason_map = {}
966
- for feature in features:
967
- if not isinstance(feature, dict):
968
- continue
969
- fid = feature.get("id")
970
- if not fid:
971
- continue
972
- fs = load_feature_status(state_dir, fid)
973
- dr = fs.get("degraded_reason")
974
- if dr:
975
- degraded_reason_map[fid] = dr
976
-
977
- for feature in features:
978
- if not isinstance(feature, dict):
979
- continue
980
- fid = feature.get("id")
981
- title = feature.get("title", "Untitled")
982
- if not fid:
983
- continue
984
-
985
- fstatus = feature.get("status", "pending")
986
- degraded_reason = degraded_reason_map.get(fid)
987
-
988
- # Count statuses
989
- if fstatus in counts:
990
- counts[fstatus] += 1
991
- else:
992
- counts["pending"] += 1
993
-
994
- # Build status indicator and color
995
- # Show degraded reason via icon when a pending feature has one
996
- if fstatus == "pending" and degraded_reason == "commit_missing":
997
- icon = COLOR_RED + "[↑]" + COLOR_RESET
998
- elif fstatus == "pending" and degraded_reason == "docs_missing":
999
- icon = COLOR_RED + "[D]" + COLOR_RESET
1000
- elif fstatus == "pending" and degraded_reason == "merge_conflict":
1001
- icon = COLOR_RED + "[⚡]" + COLOR_RESET
1002
- elif fstatus == "completed":
1003
- icon = COLOR_GREEN + "[✓]" + COLOR_RESET
1004
- elif fstatus == "in_progress":
1005
- icon = COLOR_YELLOW + "[→]" + COLOR_RESET
1006
- elif fstatus == "failed":
1007
- icon = COLOR_RED + "[✗]" + COLOR_RESET
1008
- elif fstatus == "skipped":
1009
- icon = COLOR_GRAY + "[—]" + COLOR_RESET
1010
- elif fstatus == "auto_skipped":
1011
- icon = COLOR_GRAY + "[⊘]" + COLOR_RESET
1012
- else:
1013
- icon = COLOR_GRAY + "[ ]" + COLOR_RESET
1014
-
1015
- # Build detail suffix
1016
- detail = ""
1017
- if fstatus == "pending" and degraded_reason:
1018
- detail = " ({}, retrying)".format(degraded_reason)
1019
- # Also check if blocked by dependencies
1020
- deps = feature.get("dependencies", [])
1021
- blocking = [
1022
- d for d in deps
1023
- if status_map.get(d, "pending") != "completed"
1024
- ]
1025
- if blocking:
1026
- detail = " ({}, blocked by {})".format(degraded_reason, ", ".join(blocking))
1027
- elif fstatus == "pending":
1028
- # Check if blocked by dependencies
1029
- deps = feature.get("dependencies", [])
1030
- blocking = [
1031
- d for d in deps
1032
- if status_map.get(d, "pending") != "completed"
1033
- ]
1034
- if blocking:
1035
- detail = " (blocked by {})".format(", ".join(blocking))
1036
- elif fstatus == "auto_skipped":
1037
- deps = feature.get("dependencies", [])
1038
- blockers = [
1039
- d for d in deps
1040
- if status_map.get(d, "pending") in ("failed", "skipped", "auto_skipped")
1041
- ]
1042
- if blockers:
1043
- detail = " (auto-skipped: blocked by {})".format(", ".join(blockers))
1044
- elif fstatus == "failed" and degraded_reason:
1045
- detail = " (last failure: {})".format(degraded_reason)
1046
-
1047
- # Apply color to the whole line content
1048
- if fstatus == "completed":
1049
- line_content = "{} {} {}{}".format(
1050
- fid, icon, COLOR_GREEN + title + COLOR_RESET, detail
1051
- )
1052
- elif fstatus == "in_progress":
1053
- line_content = "{} {} {}{}".format(
1054
- fid, icon, COLOR_YELLOW + title + COLOR_RESET, detail
1055
- )
1056
- elif fstatus == "failed":
1057
- line_content = "{} {} {}{}".format(
1058
- fid, icon, COLOR_RED + title + COLOR_RESET, detail
1059
- )
1060
- elif degraded_reason:
1061
- line_content = "{} {} {}{}".format(
1062
- fid, icon, COLOR_RED + title + COLOR_RESET, detail
1063
- )
1064
- else:
1065
- line_content = "{} {} {}{}".format(
1066
- fid, icon, COLOR_GRAY + title + COLOR_RESET, detail
1067
- )
1068
-
1069
- feature_lines.append(line_content)
1070
-
1071
- total = len(features)
1072
- completed = counts["completed"]
1073
-
1074
- # Calculate percentage
1075
- if total > 0:
1076
- percent = round(completed / total * 100, 1)
1077
- else:
1078
- percent = 0.0
1079
-
1080
- # Generate progress bar
1081
- progress_bar = _build_progress_bar(percent, width=24)
1082
-
1083
- # Estimate remaining time
1084
- est_remaining, confidence = _estimate_remaining_time(
1085
- features, state_dir, counts, feature_list_data
1086
- )
1087
-
1088
- summary_line = "Total: {} features | Completed: {} | In Progress: {}".format(
1089
- total, completed, counts["in_progress"]
1090
- )
1091
- summary_line2 = "Failed: {} | Pending: {} | Skipped: {} | Auto-skipped: {}".format(
1092
- counts["failed"], counts["pending"], counts["skipped"], counts["auto_skipped"]
1093
- )
1094
-
1095
- # Count degraded features (pending with a degraded_reason from status.json)
1096
- degraded_count = sum(
1097
- 1 for fid, dr in degraded_reason_map.items()
1098
- if status_map.get(fid) == "pending" and dr
1099
- )
1100
- if degraded_count > 0:
1101
- summary_line3 = "Degraded (retrying): {}".format(degraded_count)
1102
- else:
1103
- summary_line3 = None
1104
-
1105
- # Build estimated remaining time line
1106
- CONFIDENCE_ICONS = {"high": "●", "medium": "◐", "low": "○"}
1107
- if est_remaining is not None:
1108
- eta_str = _format_duration(est_remaining)
1109
- conf_icon = CONFIDENCE_ICONS.get(confidence, "○")
1110
- eta_line = "ETA: ~{} (confidence: {} {})".format(
1111
- eta_str, conf_icon, confidence
1112
- )
1113
- else:
1114
- eta_line = "ETA: calculating... (need >=1 completed feature)"
1115
-
1116
- # Print the box
1117
- inner = BOX_WIDTH - 2 # space inside the vertical bars
1118
- print("╔" + "═" * BOX_WIDTH + "╗")
1119
- print("║" + pad_right(COLOR_BOLD + " Dev-Pipeline Status" + COLOR_RESET, inner) + " ║")
1120
- print("╠" + "═" * BOX_WIDTH + "╣")
1121
- print("║" + pad_right(" Project: {}".format(app_name), inner) + " ║")
1122
- print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
1123
- print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
1124
- if summary_line3:
1125
- print("║" + pad_right(" {}".format(summary_line3), inner) + " ║")
1126
- print("╠" + "─" * BOX_WIDTH + "╣")
1127
- print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
1128
- print("║" + pad_right(" {}".format(eta_line), inner) + " ║")
1129
- print("╠" + "═" * BOX_WIDTH + "╣")
1130
- for line in feature_lines:
1131
- print("║" + pad_right(" {}".format(line), inner) + " ║")
1132
- print("╚" + "═" * BOX_WIDTH + "╝")
1133
-
1134
-
1135
- # ---------------------------------------------------------------------------
1136
- # Action: start
1137
- # ---------------------------------------------------------------------------
1138
-
1139
- def action_start(args, feature_list_path, state_dir):
1140
- """Mark a feature as in_progress at session start.
1141
-
1142
- This keeps .prizmkit/plans/feature-list.json/state status in sync during execution,
1143
- instead of only updating after session end.
1144
- """
1145
- feature_id = args.feature_id
1146
- if not feature_id:
1147
- error_out("--feature-id is required for 'start' action")
1148
- return
1149
-
1150
- fs = load_feature_status(state_dir, feature_id)
1151
- old_status = get_feature_status_from_list(feature_list_path, feature_id)
1152
-
1153
- fs["updated_at"] = now_iso()
1154
-
1155
- err = save_feature_status(state_dir, feature_id, fs)
1156
- if err:
1157
- error_out("Failed to save feature status: {}".format(err))
1158
- return
1159
-
1160
- err = update_feature_in_list(feature_list_path, feature_id, "in_progress")
1161
- if err:
1162
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
1163
- return
1164
-
1165
- result = {
1166
- "action": "start",
1167
- "feature_id": feature_id,
1168
- "old_status": old_status,
1169
- "new_status": "in_progress",
1170
- "retry_count": fs.get("retry_count", 0),
1171
- "infra_error_count": fs.get("infra_error_count", 0),
1172
- "updated_at": fs["updated_at"],
1173
- }
1174
- print(json.dumps(result, indent=2, ensure_ascii=False))
1175
-
1176
-
1177
- # ---------------------------------------------------------------------------
1178
- # Action: reset
1179
- # ---------------------------------------------------------------------------
1180
-
1181
- def action_reset(args, feature_list_path, state_dir):
1182
- """Reset a feature to pending state.
1183
-
1184
- Resets status.json runtime fields (retry_count -> 0, clear sessions,
1185
- clear resume_from_phase) and updates .prizmkit/plans/feature-list.json status to pending.
1186
- Does NOT delete any files on disk.
1187
- """
1188
- feature_id = args.feature_id
1189
- if not feature_id:
1190
- error_out("--feature-id is required for 'reset' action")
1191
- return
1192
-
1193
- # Load current status to preserve created_at
1194
- fs = load_feature_status(state_dir, feature_id)
1195
- old_status = get_feature_status_from_list(feature_list_path, feature_id)
1196
- old_retry = fs.get("retry_count", 0)
1197
-
1198
- # Reset runtime fields
1199
- fs["retry_count"] = 0
1200
- fs["infra_error_count"] = 0
1201
- fs["last_infra_error_session_id"] = None
1202
- fs["sessions"] = []
1203
- fs["last_session_id"] = None
1204
- fs["resume_from_phase"] = None
1205
- fs["updated_at"] = now_iso()
1206
-
1207
- # Write back status.json
1208
- err = save_feature_status(state_dir, feature_id, fs)
1209
- if err:
1210
- error_out("Failed to save feature status: {}".format(err))
1211
- return
1212
-
1213
- # Update .prizmkit/plans/feature-list.json
1214
- err = update_feature_in_list(feature_list_path, feature_id, "pending")
1215
- if err:
1216
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
1217
- return
1218
-
1219
- result = {
1220
- "action": "reset",
1221
- "feature_id": feature_id,
1222
- "old_status": old_status,
1223
- "old_retry_count": old_retry,
1224
- "new_status": "pending",
1225
- }
1226
- print(json.dumps(result, indent=2, ensure_ascii=False))
1227
-
1228
-
1229
- # ---------------------------------------------------------------------------
1230
- # Action: clean
1231
- # ---------------------------------------------------------------------------
1232
-
1233
- def action_clean(args, feature_list_path, state_dir):
1234
- """Reset a feature AND delete all associated artifacts.
1235
-
1236
- Deletes:
1237
- - state/features/F-XXX/sessions/ (all session history)
1238
- - .prizmkit/specs/{slug}/ (spec, plan, tasks, contracts)
1239
-
1240
- Then performs a full reset (same as action_reset).
1241
- """
1242
- feature_id = args.feature_id
1243
- feature_slug = args.feature_slug
1244
- project_root = args.project_root
1245
-
1246
- if not feature_id:
1247
- error_out("--feature-id is required for 'clean' action")
1248
- return
1249
- if not feature_slug:
1250
- error_out("--feature-slug is required for 'clean' action")
1251
- return
1252
- if not project_root:
1253
- error_out("--project-root is required for 'clean' action")
1254
- return
1255
-
1256
- cleaned = []
1257
-
1258
- # 1. Delete session history
1259
- sessions_dir = os.path.join(state_dir, feature_id, "sessions")
1260
- sessions_deleted = 0
1261
- if os.path.isdir(sessions_dir):
1262
- for entry in os.listdir(sessions_dir):
1263
- entry_path = os.path.join(sessions_dir, entry)
1264
- if os.path.isdir(entry_path):
1265
- shutil.rmtree(entry_path)
1266
- sessions_deleted += 1
1267
- cleaned.append("Deleted {} session(s) from {}".format(
1268
- sessions_deleted, sessions_dir
1269
- ))
1270
-
1271
- # 2. Delete prizmkit specs for this feature
1272
- specs_dir = os.path.join(project_root, ".prizmkit", "specs", feature_slug)
1273
- if os.path.isdir(specs_dir):
1274
- file_count = sum(
1275
- len(files) for _, _, files in os.walk(specs_dir)
1276
- )
1277
- shutil.rmtree(specs_dir)
1278
- cleaned.append("Deleted {} ({} files)".format(specs_dir, file_count))
1279
-
1280
- # 3. Delete global dev-team workspace (shared AI transient context)
1281
- dev_team_dir = os.path.join(project_root, ".dev-team")
1282
- if os.path.isdir(dev_team_dir):
1283
- file_count = sum(len(files) for _, _, files in os.walk(dev_team_dir))
1284
- shutil.rmtree(dev_team_dir)
1285
- cleaned.append("Deleted {} ({} files)".format(dev_team_dir, file_count))
1286
-
1287
- # 4. (removed: current-session.json no longer used)
1288
-
1289
- # 5. Reset status (reuse reset logic)
1290
- fs = load_feature_status(state_dir, feature_id)
1291
- old_status = get_feature_status_from_list(feature_list_path, feature_id)
1292
- old_retry = fs.get("retry_count", 0)
1293
-
1294
- fs["retry_count"] = 0
1295
- fs["infra_error_count"] = 0
1296
- fs["last_infra_error_session_id"] = None
1297
- fs["sessions"] = []
1298
- fs["last_session_id"] = None
1299
- fs["resume_from_phase"] = None
1300
- fs["updated_at"] = now_iso()
1301
-
1302
- err = save_feature_status(state_dir, feature_id, fs)
1303
- if err:
1304
- error_out("Failed to save feature status: {}".format(err))
1305
- return
1306
-
1307
- err = update_feature_in_list(feature_list_path, feature_id, "pending")
1308
- if err:
1309
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
1310
- return
1311
-
1312
- result = {
1313
- "action": "clean",
1314
- "feature_id": feature_id,
1315
- "feature_slug": feature_slug,
1316
- "old_status": old_status,
1317
- "old_retry_count": old_retry,
1318
- "new_status": "pending",
1319
- "sessions_deleted": sessions_deleted,
1320
- "cleaned": cleaned,
1321
- }
1322
- print(json.dumps(result, indent=2, ensure_ascii=False))
1323
-
1324
-
1325
- # ---------------------------------------------------------------------------
1326
- # Action: unskip
1327
- # ---------------------------------------------------------------------------
1328
-
1329
- def action_unskip(args, feature_list_path, state_dir):
1330
- """Recover auto-skipped features by resetting them and their failed upstream.
1331
-
1332
- Two modes:
1333
- - --feature-id F-032: Reset the specified failed/skipped feature + all auto_skipped
1334
- features whose dependency chain includes it.
1335
- - No --feature-id: Reset ALL failed, skipped, and auto_skipped features to pending.
1336
- """
1337
- feature_id = args.feature_id
1338
-
1339
- data, err = load_json_file(feature_list_path)
1340
- if err:
1341
- error_out("Cannot load feature list: {}".format(err))
1342
- return
1343
- features = data.get("features", [])
1344
-
1345
- to_reset = set()
1346
-
1347
- if feature_id:
1348
- # Find the target feature
1349
- target = None
1350
- for f in features:
1351
- if isinstance(f, dict) and f.get("id") == feature_id:
1352
- target = f
1353
- break
1354
- if not target:
1355
- error_out("Feature '{}' not found in .prizmkit/plans/feature-list.json".format(feature_id))
1356
- return
1357
- if target.get("status") not in ("failed", "skipped", "auto_skipped"):
1358
- error_out(
1359
- "Feature '{}' has status '{}', expected 'failed', 'skipped', or 'auto_skipped'".format(
1360
- feature_id, target.get("status", "unknown")
1361
- )
1362
- )
1363
- return
1364
-
1365
- # If target is failed or skipped, reset it and find all auto_skipped descendants
1366
- if target.get("status") in ("failed", "skipped"):
1367
- to_reset.add(feature_id)
1368
- # Find all auto_skipped features that depend (transitively) on this one
1369
- changed = True
1370
- while changed:
1371
- changed = False
1372
- for f in features:
1373
- if not isinstance(f, dict):
1374
- continue
1375
- fid = f.get("id")
1376
- if not fid or fid in to_reset:
1377
- continue
1378
- if f.get("status") != "auto_skipped":
1379
- continue
1380
- deps = f.get("dependencies", [])
1381
- if any(d in to_reset for d in deps):
1382
- to_reset.add(fid)
1383
- changed = True
1384
-
1385
- # If target is auto_skipped, reset it and its failed upstream + siblings
1386
- elif target.get("status") == "auto_skipped":
1387
- to_reset.add(feature_id)
1388
- # Transitively walk upstream to find ALL failed/auto_skipped ancestors
1389
- # (e.g., F-001 failed → F-002 auto_skipped → F-003 auto_skipped;
1390
- # unskip F-003 must also find and reset F-001)
1391
- upstream_changed = True
1392
- while upstream_changed:
1393
- upstream_changed = False
1394
- for f in features:
1395
- if not isinstance(f, dict):
1396
- continue
1397
- fid = f.get("id")
1398
- if not fid or fid not in to_reset:
1399
- continue
1400
- for dep_id in f.get("dependencies", []):
1401
- if dep_id in to_reset:
1402
- continue
1403
- for dep_f in features:
1404
- if isinstance(dep_f, dict) and dep_f.get("id") == dep_id:
1405
- if dep_f.get("status") in ("failed", "skipped", "auto_skipped"):
1406
- to_reset.add(dep_id)
1407
- upstream_changed = True
1408
- # Also reset downstream auto_skipped features blocked by the same upstreams
1409
- changed = True
1410
- while changed:
1411
- changed = False
1412
- for f in features:
1413
- if not isinstance(f, dict):
1414
- continue
1415
- fid = f.get("id")
1416
- if not fid or fid in to_reset:
1417
- continue
1418
- if f.get("status") != "auto_skipped":
1419
- continue
1420
- fdeps = f.get("dependencies", [])
1421
- if any(d in to_reset for d in fdeps):
1422
- to_reset.add(fid)
1423
- changed = True
1424
- else:
1425
- # No feature-id: reset ALL failed + skipped + auto_skipped
1426
- for f in features:
1427
- if isinstance(f, dict) and f.get("id"):
1428
- if f.get("status") in ("failed", "skipped", "auto_skipped"):
1429
- to_reset.add(f["id"])
1430
-
1431
- if not to_reset:
1432
- error_out("No features to unskip")
1433
- return
1434
-
1435
- # Reset all collected features in .prizmkit/plans/feature-list.json
1436
- reset_details = []
1437
- for f in features:
1438
- if isinstance(f, dict) and f.get("id") in to_reset:
1439
- old_status = f.get("status", "unknown")
1440
- f["status"] = "pending"
1441
- reset_details.append({
1442
- "feature_id": f["id"],
1443
- "title": f.get("title", ""),
1444
- "old_status": old_status,
1445
- })
1446
-
1447
- err = write_json_file(feature_list_path, data)
1448
- if err:
1449
- error_out("Failed to write .prizmkit/plans/feature-list.json: {}".format(err))
1450
- return
1451
-
1452
- # Reset runtime fields in status.json for each feature
1453
- for fid in to_reset:
1454
- fs = load_feature_status(state_dir, fid)
1455
- fs["retry_count"] = 0
1456
- fs["infra_error_count"] = 0
1457
- fs["last_infra_error_session_id"] = None
1458
- fs["sessions"] = []
1459
- fs["last_session_id"] = None
1460
- fs["resume_from_phase"] = None
1461
- fs["updated_at"] = now_iso()
1462
- save_feature_status(state_dir, fid, fs)
1463
-
1464
- result = {
1465
- "action": "unskip",
1466
- "reset_count": len(to_reset),
1467
- "features": reset_details,
1468
- }
1469
- print(json.dumps(result, indent=2, ensure_ascii=False))
1470
-
1471
-
1472
- # ---------------------------------------------------------------------------
1473
- # Action: pause
1474
- # ---------------------------------------------------------------------------
1475
-
1476
- def action_pause(state_dir):
1477
- """Save current pipeline state for graceful shutdown."""
1478
- pipeline_path = os.path.join(state_dir, "pipeline.json")
1479
-
1480
- data, err = load_json_file(pipeline_path)
1481
- if err:
1482
- # If pipeline.json doesn't exist, create a minimal one
1483
- data = {
1484
- "status": "paused",
1485
- "paused_at": now_iso(),
1486
- }
1487
- else:
1488
- data["status"] = "paused"
1489
- data["paused_at"] = now_iso()
1490
-
1491
- err = write_json_file(pipeline_path, data)
1492
- if err:
1493
- error_out("Failed to write pipeline.json: {}".format(err))
1494
- return
1495
-
1496
- result = {
1497
- "action": "pause",
1498
- "status": "paused",
1499
- "paused_at": data["paused_at"],
1500
- }
1501
- print(json.dumps(result, indent=2, ensure_ascii=False))
1502
-
1503
-
1504
- # ---------------------------------------------------------------------------
1505
- # Main
1506
- # ---------------------------------------------------------------------------
1507
-
1508
- def main():
1509
- args = parse_args()
1510
-
1511
- # Validate action-specific requirements
1512
- if args.action == "update":
1513
- if not args.feature_id:
1514
- error_out("--feature-id is required for 'update' action")
1515
- if not args.session_status:
1516
- error_out("--session-status is required for 'update' action")
1517
- if args.action in ("start", "reset", "clean", "complete"):
1518
- if not args.feature_id:
1519
- error_out("--feature-id is required for '{}' action".format(args.action))
1520
- if args.action == "clean":
1521
- if not args.feature_slug:
1522
- error_out("--feature-slug is required for 'clean' action")
1523
- if not args.project_root:
1524
- error_out("--project-root is required for 'clean' action")
1525
-
1526
- # Load feature list
1527
- feature_list_data, err = load_json_file(args.feature_list)
1528
- if err:
1529
- error_out("Cannot load feature list: {}".format(err))
1530
-
1531
- # Parse feature filter (used by get_next and status)
1532
- feature_filter = parse_feature_filter(args.features)
1533
-
1534
- # Dispatch action
1535
- if args.action == "get_next":
1536
- action_get_next(feature_list_data, args.state_dir, feature_filter)
1537
- elif args.action == "start":
1538
- action_start(args, args.feature_list, args.state_dir)
1539
- elif args.action == "update":
1540
- action_update(args, args.feature_list, args.state_dir)
1541
- elif args.action == "status":
1542
- action_status(feature_list_data, args.state_dir, feature_filter)
1543
- elif args.action == "reset":
1544
- action_reset(args, args.feature_list, args.state_dir)
1545
- elif args.action == "clean":
1546
- action_clean(args, args.feature_list, args.state_dir)
1547
- elif args.action == "complete":
1548
- # Shortcut: 'complete' is equivalent to 'update --session-status success'
1549
- args.session_status = "success"
1550
- action_update(args, args.feature_list, args.state_dir)
1551
- elif args.action == "pause":
1552
- action_pause(args.state_dir)
1553
- elif args.action == "unskip":
1554
- action_unskip(args, args.feature_list, args.state_dir)
1555
-
1556
-
1557
- if __name__ == "__main__":
1558
- main()