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,1232 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- parse-stream-progress.py - Real-time stream-json progress parser
4
-
5
- Continuously reads an AI CLI session log (JSONL from --output-format stream-json),
6
- extracts tool calls, phase changes, and activity metrics, and writes structured
7
- progress to a progress.json file for heartbeat monitoring.
8
-
9
- Usage:
10
- python3 parse-stream-progress.py --session-log <path> --progress-file <path>
11
-
12
- The script runs until:
13
- - The session log stops growing and the CLI process exits
14
- - It receives SIGTERM/SIGINT
15
- """
16
-
17
- import argparse
18
- import json
19
- import os
20
- import re
21
- import signal
22
- import sys
23
- import tempfile
24
- import time
25
- from collections import Counter
26
- from datetime import datetime, timezone
27
- from pathlib import Path
28
-
29
-
30
- # Ordered pipeline phases — index defines forward-only progression.
31
- # Phase detection is monotonic: once a phase is reached, earlier phases
32
- # cannot be re-entered (prevents false positives from file content mentions).
33
- PHASE_ORDER = ["plan", "implement", "code-review", "retrospective", "commit"]
34
-
35
- # Keywords for phase detection.
36
- # "strong" keywords are skill invocations — high confidence, but still
37
- # forward-only (a mention of prizmkit-plan while in commit phase is noise).
38
- # "weak" keywords are contextual hints — also forward-only, used when no
39
- # strong keyword matches.
40
- PHASE_KEYWORDS = {
41
- "plan": {
42
- "strong": ["prizmkit-plan"],
43
- "weak": ["spec.md", "plan.md", "specification", "architecture", "task checklist", "task breakdown", "gathering requirements"],
44
- },
45
- "implement": {
46
- "strong": ["prizmkit-implement"],
47
- "weak": ["implement", "writing code", "TDD", "coding"],
48
- },
49
- "code-review": {
50
- "strong": ["prizmkit-code-review"],
51
- "weak": ["code review", "review verdict", "reviewing"],
52
- },
53
- "retrospective": {
54
- "strong": ["prizmkit-retrospective"],
55
- "weak": ["retrospective", "structural sync", "architecture knowledge", ".prizmkit/prizm-docs/ sync"],
56
- },
57
- "commit": {
58
- "strong": ["prizmkit-committer"],
59
- "weak": ["git commit", "committing", "feat(", "fix("],
60
- },
61
- }
62
-
63
- CONTEXT_ERROR_PATTERNS = [
64
- re.compile(pattern, re.IGNORECASE)
65
- for pattern in (
66
- r"context_too_large",
67
- r"model_context_window_exceeded",
68
- r"Your input exceeds the context window",
69
- r"input exceeds the context window",
70
- r"context window of this model",
71
- r"context window exceeded",
72
- r"invalid_request_error.*context window",
73
- r"context window.*invalid_request_error",
74
- )
75
- ]
76
-
77
- ERROR_CONTEXT_PATTERNS = [
78
- re.compile(pattern, re.IGNORECASE)
79
- for pattern in (
80
- r"\bapi error\b",
81
- r"invalid_request_error",
82
- r"\bstatus\s*[:=]?\s*(400|413)\b",
83
- r"\bapi_error_status\b",
84
- r"\bapi_error_code\b",
85
- r"\blast_result_is_error\b\s*[\"':=]*\s*true\b",
86
- r"\bis_error\b\s*[\"':=]*\s*true\b",
87
- )
88
- ]
89
-
90
-
91
- def _has_error_context(text):
92
- """Return true when free text looks like a runtime/provider error."""
93
- if not text:
94
- return False
95
- return any(pattern.search(text) for pattern in ERROR_CONTEXT_PATTERNS)
96
-
97
-
98
- def detect_api_error_code(text, require_error_context=False):
99
- """Return a normalized fatal/runtime error code from terminal text.
100
-
101
- Structured terminal result/error events and raw stderr can be matched
102
- directly. Ordinary assistant prose is noisier: it may mention the phrase
103
- "input exceeds the context window" while explaining a test or recovery
104
- rule, so callers can require additional error-like context there.
105
- """
106
- if not text:
107
- return ""
108
- if require_error_context and not _has_error_context(text):
109
- return ""
110
- for pattern in CONTEXT_ERROR_PATTERNS:
111
- if pattern.search(text):
112
- return "context_too_large"
113
- return ""
114
-
115
-
116
- class ProgressTracker:
117
- """Tracks progress state from stream-json events."""
118
-
119
- def __init__(self, session_log=None):
120
- self.session_log_path = Path(session_log).expanduser() if session_log else None
121
- self.message_count = 0
122
- self.current_tool = None
123
- self.current_tool_input_summary = ""
124
- self.current_phase = None
125
- self.detected_phases = []
126
- self.tool_call_counts = Counter()
127
- self.total_tool_calls = 0
128
- self.last_text_snippet = ""
129
- self.last_result_is_error = False
130
- self.api_error_status = None
131
- self.api_error_code = ""
132
- self.terminal_result_text = ""
133
- self.terminal_success_at = ""
134
- self.fatal_error_code = ""
135
- self.is_active = True
136
- self.errors = []
137
- self.event_format = ""
138
- self.active_subagent_count = 0
139
- self.subagent_status_counts = Counter()
140
- self._subagent_spawn_count = 0
141
- self.codex_child_thread_ids = set()
142
- self.cb_session_id = ""
143
- self.cb_cwd = ""
144
- self.claude_session_id = ""
145
- self.claude_cwd = ""
146
- self.claude_task_states = {}
147
- self.child_session_files = []
148
- self.child_total_bytes = 0
149
- self.child_activity_signature = ""
150
- self.last_child_activity_at = ""
151
- self._codex_child_session_paths = {}
152
- self._claude_child_session_files = []
153
- self._cb_child_session_files = []
154
- self._last_child_scan_at = 0.0
155
- self._last_claude_fallback_scan_at = 0.0
156
- self._last_claude_fallback_scan_key = ""
157
- self._claude_fallback_scan_interval_seconds = 10.0
158
- self._text_buffer = ""
159
- self._in_tool_use = False
160
- self._current_tool_input_parts = []
161
-
162
- def process_event(self, event):
163
- """Process a single stream-json event and update state.
164
-
165
- Supports three formats:
166
- 1. Claude API raw stream: message_start, content_block_start, content_block_delta, etc.
167
- 2. Claude Code verbose stream-json: assistant, user, tool_result, system, etc.
168
- (produced by claude/claude-internal --verbose --output-format stream-json)
169
- 3. CodeBuddy stream-json: message/function_call/function_call_result events
170
- (produced by cbc --print -y --output-format stream-json — message type
171
- with sessionId/cwd metadata, function_call for tool invocations).
172
- """
173
- event_type = event.get("type", "")
174
-
175
- # ── Codex exec --json JSONL format ──────────────────────────
176
- if event_type in (
177
- "thread.started", "turn.started", "turn.completed",
178
- "turn.failed", "item.started", "item.completed", "error",
179
- ):
180
- self.event_format = "codex-json"
181
- self.is_active = True
182
-
183
- if event_type == "turn.started":
184
- self.message_count += 1
185
-
186
- elif event_type in ("item.started", "item.completed"):
187
- item = event.get("item", {})
188
- item_type = item.get("type", "")
189
-
190
- if item_type == "agent_message":
191
- text = item.get("text", "")
192
- if text.strip():
193
- self.last_text_snippet = text.strip()[:120]
194
- self._detect_phase(text)
195
-
196
- elif item_type == "collab_tool_call":
197
- tool_name = item.get("tool", "collab")
198
- self._record_codex_child_thread_ids(
199
- item.get("receiver_thread_ids")
200
- )
201
- if event_type == "item.started":
202
- self.current_tool = tool_name
203
- self.tool_call_counts[tool_name] += 1
204
- self.total_tool_calls += 1
205
- elif item.get("status") == "completed":
206
- self.current_tool = None
207
- self._extract_tool_summary_from_dict(item)
208
- self._update_subagent_status_counts(
209
- item.get("agents_states", {})
210
- )
211
-
212
- prompt = item.get("prompt")
213
- if prompt:
214
- self._detect_phase(prompt)
215
-
216
- else:
217
- tool_name = item.get("tool") or item.get("name")
218
- if tool_name:
219
- if event_type == "item.started":
220
- self.current_tool = tool_name
221
- self.tool_call_counts[tool_name] += 1
222
- self.total_tool_calls += 1
223
- elif item.get("status") == "completed":
224
- self.current_tool = None
225
- self._extract_tool_summary_from_dict(item)
226
-
227
- elif event_type == "turn.completed":
228
- self.current_tool = None
229
-
230
- elif event_type == "turn.failed":
231
- error = event.get("error") or event.get("message") or "Codex turn failed"
232
- self.errors.append(str(error))
233
- self._detect_terminal_error(str(error))
234
- self.current_tool = None
235
-
236
- elif event_type == "error":
237
- error = event.get("error") or event.get("message") or "Unknown error"
238
- self.errors.append(str(error))
239
- self._detect_terminal_error(str(error))
240
-
241
- return
242
-
243
- # ── CodeBuddy stream-json format ──────────────────────────────
244
- # cbc --print -y --output-format stream-json emits:
245
- # message (role=user/assistant), function_call, function_call_result,
246
- # file-history-snapshot, error. The first user message carries
247
- # sessionId and cwd metadata; later function_call items carry the
248
- # same fields.
249
- # NOTE: cbc emits tool invocations as EITHER tool_use blocks inside a
250
- # message (role=assistant) event OR as standalone function_call events,
251
- # but not both for the same invocation. The two handlers are
252
- # complementary rather than redundant, so no deduplication is needed.
253
- # Detect via event_type=="message" with a "sessionId" field.
254
- # Must run before the Claude Code assistant / tool_result branches
255
- # because those also accept those types but use different internals.
256
- if event_type == "message" and isinstance(event.get("sessionId"), str):
257
- self.event_format = self.event_format or "stream-json"
258
- role = event.get("role", "")
259
- sid = event.get("sessionId", "")
260
- cwd = event.get("cwd", "")
261
- if sid and not self.cb_session_id:
262
- self.cb_session_id = sid.strip()
263
- if cwd and not self.cb_cwd:
264
- self.cb_cwd = cwd.strip()
265
- if role == "assistant":
266
- self.message_count += 1
267
- self.is_active = True
268
- content = event.get("content", [])
269
- if isinstance(content, list):
270
- for block in content:
271
- block_type = block.get("type", "")
272
- if block_type == "tool_use":
273
- tool_name = block.get("name", "unknown")
274
- self.current_tool = tool_name
275
- self.tool_call_counts[tool_name] += 1
276
- self.total_tool_calls += 1
277
- tool_input = block.get("input", {})
278
- if isinstance(tool_input, dict):
279
- self._extract_tool_summary_from_dict(tool_input)
280
- self._detect_phase(
281
- json.dumps(tool_input, ensure_ascii=False)[:500]
282
- )
283
- # Track CodeBuddy Agent/Task tool invocations as
284
- # potential sub-agent spawns (Gap 3 fix).
285
- self._record_cb_agent_tool_call(tool_name, tool_input)
286
- elif block_type == "text":
287
- text = block.get("text", "")
288
- if text.strip():
289
- self.last_text_snippet = text.strip()[:120]
290
- self._detect_phase(text)
291
- self._detect_terminal_error(
292
- text, require_error_context=True
293
- )
294
- return
295
-
296
- # CodeBuddy function_call / function_call_result in stream-json
297
- if event_type in ("function_call", "function_call_result"):
298
- self.event_format = self.event_format or "stream-json"
299
- sid = event.get("sessionId", "")
300
- cwd = event.get("cwd", "")
301
- if sid and not self.cb_session_id:
302
- self.cb_session_id = sid.strip()
303
- if cwd and not self.cb_cwd:
304
- self.cb_cwd = cwd.strip()
305
- if event_type == "function_call":
306
- tool_name = event.get("name", "unknown")
307
- self.current_tool = tool_name
308
- self.tool_call_counts[tool_name] += 1
309
- self.total_tool_calls += 1
310
- self.is_active = True
311
- # Extract summary from arguments
312
- raw_args = event.get("arguments", "")
313
- if isinstance(raw_args, str) and raw_args.strip():
314
- self._extract_tool_summary(raw_args)
315
- self._detect_phase(raw_args[:500])
316
- # Track Agent/Task tool invocations as sub-agent spawns
317
- self._record_cb_agent_tool_call(tool_name, raw_args)
318
- elif event_type == "function_call_result":
319
- self.current_tool = None
320
- # Check result text for phase / error hints
321
- result_text = event.get("output") or ""
322
- if result_text and isinstance(result_text, str):
323
- stripped = result_text.strip()
324
- if stripped:
325
- self.last_text_snippet = stripped[:120]
326
- self._detect_phase(stripped)
327
- self._detect_terminal_error(stripped, require_error_context=True)
328
- return
329
-
330
- # ── Claude Code verbose format ──────────────────────────────
331
- if event_type == "assistant":
332
- self.event_format = self.event_format or "stream-json"
333
- self.message_count += 1
334
- self.is_active = True
335
- message = event.get("message", {})
336
- content_blocks = message.get("content", [])
337
- for block in content_blocks:
338
- block_type = block.get("type", "")
339
- if block_type == "tool_use":
340
- tool_name = block.get("name", "unknown")
341
- self.current_tool = tool_name
342
- self.tool_call_counts[tool_name] += 1
343
- self.total_tool_calls += 1
344
- # Extract summary from input
345
- tool_input = block.get("input", {})
346
- if isinstance(tool_input, dict):
347
- self._extract_tool_summary_from_dict(tool_input)
348
- self._detect_phase(json.dumps(tool_input, ensure_ascii=False)[:500])
349
- elif block_type == "text":
350
- text = block.get("text", "")
351
- if text.strip():
352
- self.last_text_snippet = text.strip()[:120]
353
- self._detect_phase(text)
354
- self._detect_terminal_error(text, require_error_context=True)
355
-
356
- elif event_type == "tool_result" or (
357
- event_type == "user"
358
- and not isinstance(event.get("sessionId"), str)
359
- ):
360
- # tool_result contains output from tool execution
361
- self.event_format = self.event_format or "stream-json"
362
- self.is_active = True
363
-
364
- # Check for error patterns in tool_result content (supports both formats):
365
- # A) Top-level tool_result events: event["content"] is the result text
366
- # B) Nested user events: event["message"]["content"][] has type=="tool_result" items
367
- content_candidates = []
368
-
369
- # Format A: top-level tool_result (Claude Code)
370
- if event_type == "tool_result":
371
- content_candidates.append(str(event.get("content", "")))
372
-
373
- # Format B: nested inside user event (Claude Code, NOT CodeBuddy)
374
- if event_type == "user":
375
- message = event.get("message", {})
376
- content_list = message.get("content", [])
377
- if isinstance(content_list, list):
378
- for item in content_list:
379
- if isinstance(item, dict) and item.get("type") == "tool_result":
380
- content_candidates.append(str(item.get("content", "")))
381
-
382
- for result_text in content_candidates:
383
- if "shorter than the provided offset" in result_text:
384
- self.errors.append({
385
- "type": "read_offset_overflow",
386
- "tool": self.current_tool,
387
- "at": datetime.now(timezone.utc).isoformat(),
388
- })
389
- break # one error per event is enough
390
- elif "Wasted call" in result_text:
391
- self.errors.append({
392
- "type": "wasted_call",
393
- "tool": self.current_tool,
394
- "at": datetime.now(timezone.utc).isoformat(),
395
- })
396
- break
397
-
398
- # Keep only last 20 errors to prevent unbounded growth in progress.json
399
- if len(self.errors) > 20:
400
- self.errors = self.errors[-20:]
401
-
402
- elif event_type == "system":
403
- # System events (hooks, init, task notifications, etc.) — track but don't count as messages.
404
- self.event_format = self.event_format or "stream-json"
405
- subtype = event.get("subtype", "")
406
- if subtype == "init":
407
- self.is_active = True
408
- session_id = event.get("session_id")
409
- if isinstance(session_id, str) and session_id.strip():
410
- self.claude_session_id = session_id.strip()
411
- cwd = event.get("cwd")
412
- if isinstance(cwd, str) and cwd.strip():
413
- self.claude_cwd = cwd.strip()
414
- elif subtype == "task_started":
415
- task_id = event.get("task_id")
416
- if isinstance(task_id, str) and task_id.strip():
417
- self.claude_task_states[task_id.strip()] = {
418
- "status": "running",
419
- "summary": str(event.get("description") or "")[:120],
420
- "tool_use_id": str(event.get("tool_use_id") or ""),
421
- "task_type": str(event.get("task_type") or ""),
422
- "subagent_type": str(event.get("subagent_type") or ""),
423
- }
424
- self._update_claude_subagent_status_counts()
425
- elif subtype in ("task_updated", "task_progress"):
426
- task_id = event.get("task_id")
427
- if isinstance(task_id, str) and task_id.strip():
428
- state = self.claude_task_states.setdefault(task_id.strip(), {})
429
- patch = event.get("patch") if isinstance(event.get("patch"), dict) else {}
430
- status = patch.get("status") or event.get("status")
431
- if status:
432
- state["status"] = str(status)
433
- summary = patch.get("summary") or patch.get("description") or event.get("summary") or event.get("description")
434
- if summary:
435
- state["summary"] = str(summary)[:120]
436
- else:
437
- state.setdefault("summary", "")
438
- tool_use_id = patch.get("tool_use_id") or event.get("tool_use_id")
439
- if tool_use_id:
440
- state["tool_use_id"] = str(tool_use_id)
441
- else:
442
- state.setdefault("tool_use_id", "")
443
- task_type = patch.get("task_type") or event.get("task_type")
444
- if task_type:
445
- state["task_type"] = str(task_type)
446
- else:
447
- state.setdefault("task_type", "")
448
- subagent_type = patch.get("subagent_type") or event.get("subagent_type")
449
- if subagent_type:
450
- state["subagent_type"] = str(subagent_type)
451
- else:
452
- state.setdefault("subagent_type", "")
453
- self._update_claude_subagent_status_counts()
454
- elif subtype == "task_notification":
455
- task_id = event.get("task_id")
456
- if isinstance(task_id, str) and task_id.strip():
457
- state = self.claude_task_states.setdefault(task_id.strip(), {})
458
- status = event.get("status") or "completed"
459
- state["status"] = str(status)
460
- state["summary"] = str(event.get("summary") or state.get("summary") or "")[:120]
461
- state.setdefault("tool_use_id", str(event.get("tool_use_id") or ""))
462
- task_type = event.get("task_type")
463
- if task_type:
464
- state["task_type"] = str(task_type)
465
- else:
466
- state.setdefault("task_type", "")
467
- subagent_type = event.get("subagent_type")
468
- if subagent_type:
469
- state["subagent_type"] = str(subagent_type)
470
- else:
471
- state.setdefault("subagent_type", "")
472
- self._update_claude_subagent_status_counts()
473
-
474
- elif event_type == "result":
475
- self.event_format = self.event_format or "stream-json"
476
- self.is_active = False
477
- result_text = event.get("result") or event.get("message") or ""
478
- error_obj = event.get("error")
479
- if isinstance(error_obj, dict):
480
- error_text = " ".join(
481
- str(error_obj.get(key) or "")
482
- for key in ("type", "code", "message")
483
- if error_obj.get(key)
484
- )
485
- result_text = " ".join(part for part in (str(result_text), error_text) if part)
486
- api_error_code = event.get("api_error_code") or event.get("error_code") or ""
487
- if isinstance(error_obj, dict) and not api_error_code:
488
- api_error_code = error_obj.get("code") or error_obj.get("type") or ""
489
- self._record_terminal_result(
490
- text=str(result_text or ""),
491
- is_error=bool(event.get("is_error")),
492
- api_error_status=event.get("api_error_status"),
493
- api_error_code=str(api_error_code or ""),
494
- )
495
-
496
- # ── Claude API raw stream format ────────────────────────────
497
- elif event_type == "message_start":
498
- self.event_format = self.event_format or "stream-json"
499
- self.message_count += 1
500
- self.is_active = True
501
-
502
- elif event_type == "message_stop":
503
- self.current_tool = None
504
- self.current_tool_input_summary = ""
505
- self._in_tool_use = False
506
- self._current_tool_input_parts = []
507
-
508
- elif event_type == "content_block_start":
509
- content_block = event.get("content_block", {})
510
- block_type = content_block.get("type", "")
511
-
512
- if block_type == "tool_use":
513
- tool_name = content_block.get("name", "unknown")
514
- self.current_tool = tool_name
515
- self.current_tool_input_summary = ""
516
- self.tool_call_counts[tool_name] += 1
517
- self.total_tool_calls += 1
518
- self._in_tool_use = True
519
- self._current_tool_input_parts = []
520
-
521
- elif block_type == "text":
522
- self._text_buffer = ""
523
- self._in_tool_use = False
524
-
525
- elif event_type == "content_block_delta":
526
- delta = event.get("delta", {})
527
- delta_type = delta.get("type", "")
528
-
529
- if delta_type == "text_delta":
530
- text = delta.get("text", "")
531
- self._text_buffer += text
532
- # Keep last meaningful snippet
533
- stripped = text.strip()
534
- if stripped:
535
- self.last_text_snippet = stripped[:120]
536
- # Try to detect phase from text
537
- self._detect_phase(text)
538
- self._detect_terminal_error(text, require_error_context=True)
539
-
540
- elif delta_type == "input_json_delta":
541
- partial = delta.get("partial_json", "")
542
- self._current_tool_input_parts.append(partial)
543
- # Build a summary from accumulated input
544
- accumulated = "".join(self._current_tool_input_parts)
545
- self.current_tool_input_summary = accumulated[:150]
546
-
547
- elif event_type == "content_block_stop":
548
- if self._in_tool_use:
549
- # Try to extract a better summary from complete tool input
550
- full_input = "".join(self._current_tool_input_parts)
551
- self._extract_tool_summary(full_input)
552
- self._detect_phase(full_input)
553
- else:
554
- # Text block finished - detect phase and terminal errors from accumulated text
555
- if self._text_buffer:
556
- self._detect_phase(self._text_buffer)
557
- self._detect_terminal_error(
558
- self._text_buffer,
559
- require_error_context=True,
560
- )
561
- self._in_tool_use = False
562
- self._current_tool_input_parts = []
563
-
564
- elif event_type == "error":
565
- self.event_format = self.event_format or "stream-json"
566
- error_msg = event.get("error", {}).get("message", "Unknown error")
567
- errors = event.get("errors") or []
568
- if isinstance(errors, list) and errors:
569
- error_msg = "; ".join(str(e) for e in errors[:3])
570
- self.errors.append(error_msg)
571
- self._detect_terminal_error(str(error_msg))
572
-
573
- # ── CodeBuddy file-history-snapshot (ignore) ─────────────────
574
- elif event_type == "file-history-snapshot":
575
- return
576
-
577
- # Check for subagent indicator
578
- if event.get("parent_tool_use_id"):
579
- # This is a sub-agent event; tool name is still tracked normally
580
- pass
581
-
582
- def _record_cb_agent_tool_call(self, tool_name, raw_args):
583
- """Record a CodeBuddy Agent/Task* tool invocation for sub-agent tracking.
584
-
585
- CodeBuddy spawns sub-agents via:
586
- - Agent(prompt=..., run_in_background=True/False) — synchronous or bg fork
587
- - TaskCreate(subject=..., description=...) — scheduled task
588
-
589
- (TaskUpdate/TaskOutput exist but are lifecycle-only — they track
590
- existing tasks rather than creating new sub-agents, so we don't
591
- count them as spawns.)
592
-
593
- We don't get child session ids from these tool calls in the stream,
594
- but recording the count lets the heartbeat code in heartbeat.sh apply
595
- an extended stale-kill window just as it does for Codex spawn_agent.
596
- """
597
- if tool_name not in ("Agent", "TaskCreate"):
598
- return
599
- # Both dict (from message events) and str/JSON (from function_call events)
600
- # are supported.
601
- if isinstance(raw_args, dict):
602
- args = raw_args
603
- elif isinstance(raw_args, str):
604
- try:
605
- args = json.loads(raw_args)
606
- except (json.JSONDecodeError, TypeError):
607
- return
608
- else:
609
- return
610
- # For Agent tool, subagent_type or run_in_background hints at delegation
611
- if tool_name == "Agent":
612
- subagent_type = args.get("subagent_type", "")
613
- prompt = args.get("prompt", "")
614
- if subagent_type or prompt:
615
- self._subagent_spawn_count += 1
616
- elif tool_name == "TaskCreate":
617
- self._subagent_spawn_count += 1
618
-
619
- def _record_terminal_result(self, text="", is_error=False, api_error_status=None, api_error_code=""):
620
- """Record a Claude Code terminal result event."""
621
- terminal_text = str(text or "")
622
- self.last_result_is_error = bool(is_error)
623
- if api_error_status not in (None, ""):
624
- try:
625
- self.api_error_status = int(api_error_status)
626
- except (TypeError, ValueError):
627
- self.api_error_status = api_error_status
628
- error_like_result = (
629
- self.last_result_is_error
630
- or api_error_status not in (None, "")
631
- or bool(api_error_code)
632
- or _has_error_context(terminal_text)
633
- )
634
- normalized_code = detect_api_error_code(
635
- " ".join([str(api_error_code or ""), terminal_text]),
636
- require_error_context=not error_like_result,
637
- )
638
- if normalized_code:
639
- self.api_error_code = normalized_code
640
- self.fatal_error_code = normalized_code
641
- elif api_error_code:
642
- self.api_error_code = str(api_error_code)
643
- self.terminal_result_text = terminal_text[:1000]
644
- if terminal_text.strip():
645
- self.last_text_snippet = terminal_text.strip()[:120]
646
- if not self.last_result_is_error and not self.fatal_error_code:
647
- self.terminal_success_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
648
- elif self.fatal_error_code:
649
- self.errors.append(self.fatal_error_code)
650
-
651
- def _detect_terminal_error(self, text, require_error_context=False):
652
- """Detect fatal context-window errors from unstructured text."""
653
- code = detect_api_error_code(
654
- str(text or ""),
655
- require_error_context=require_error_context,
656
- )
657
- if not code:
658
- return
659
- self.last_result_is_error = True
660
- self.api_error_code = code
661
- self.fatal_error_code = code
662
- self.terminal_result_text = str(text or "")[:1000]
663
- if text:
664
- self.last_text_snippet = str(text).strip()[:120]
665
-
666
- def _detect_phase(self, text):
667
- """Detect pipeline phase from text content.
668
-
669
- Uses monotonic (forward-only) progression to avoid false positives.
670
- Strong keywords (skill invocations) always trigger; weak keywords
671
- only trigger for phases at or ahead of the current position.
672
- """
673
- text_lower = text.lower()
674
- current_idx = (
675
- PHASE_ORDER.index(self.current_phase)
676
- if self.current_phase in PHASE_ORDER
677
- else -1
678
- )
679
-
680
- for phase, kw_groups in PHASE_KEYWORDS.items():
681
- phase_idx = PHASE_ORDER.index(phase)
682
-
683
- # Check strong keywords — high confidence but still forward-only
684
- for keyword in kw_groups.get("strong", []):
685
- if keyword.lower() in text_lower:
686
- if phase_idx >= current_idx:
687
- self.current_phase = phase
688
- if phase not in self.detected_phases:
689
- self.detected_phases.append(phase)
690
- return
691
- # Backward strong match — skip this phase but do NOT
692
- # early-return, so forward weak matches can still fire.
693
- break
694
-
695
- # Check weak keywords — only trigger for forward phases
696
- if phase_idx >= current_idx:
697
- for keyword in kw_groups.get("weak", []):
698
- if keyword.lower() in text_lower:
699
- self.current_phase = phase
700
- if phase not in self.detected_phases:
701
- self.detected_phases.append(phase)
702
- return
703
-
704
- def _extract_tool_summary(self, raw_input):
705
- """Extract a human-readable summary from tool input JSON string."""
706
- try:
707
- data = json.loads(raw_input)
708
- self._extract_tool_summary_from_dict(data)
709
- except (json.JSONDecodeError, TypeError):
710
- # Keep whatever partial summary we had
711
- pass
712
-
713
- def _extract_tool_summary_from_dict(self, data):
714
- """Extract a human-readable summary from tool input dict."""
715
- if isinstance(data, dict):
716
- # Common patterns in tool inputs
717
- if "description" in data:
718
- self.current_tool_input_summary = str(data["description"])[:100]
719
- elif "command" in data:
720
- self.current_tool_input_summary = str(data["command"])[:100]
721
- elif "file_path" in data:
722
- self.current_tool_input_summary = str(data["file_path"])[:100]
723
- elif "pattern" in data:
724
- self.current_tool_input_summary = str(data["pattern"])[:100]
725
- elif "query" in data:
726
- self.current_tool_input_summary = str(data["query"])[:100]
727
- elif "prompt" in data:
728
- self.current_tool_input_summary = str(data["prompt"])[:100]
729
-
730
- def _update_subagent_status_counts(self, agents_states):
731
- """Track Codex subagent state counts from collab_tool_call items."""
732
- counts = Counter()
733
- active = 0
734
- if isinstance(agents_states, dict):
735
- for state in agents_states.values():
736
- if not isinstance(state, dict):
737
- continue
738
- status = str(state.get("status", "unknown"))
739
- counts[status] += 1
740
- if status not in ("completed", "failed", "cancelled", "canceled"):
741
- active += 1
742
- message = state.get("message")
743
- if message:
744
- self.last_text_snippet = str(message).strip()[:120]
745
- self._detect_phase(str(message))
746
- self.subagent_status_counts = counts
747
- self.active_subagent_count = active
748
-
749
- def _record_codex_child_thread_ids(self, thread_ids):
750
- """Remember Codex child thread IDs reported by collab tool calls."""
751
- if not isinstance(thread_ids, list):
752
- return
753
- for thread_id in thread_ids:
754
- if isinstance(thread_id, str) and thread_id.strip():
755
- self.codex_child_thread_ids.add(thread_id.strip())
756
-
757
- def _codex_sessions_dir(self):
758
- """Return the Codex sessions directory for the current environment."""
759
- codex_home = os.environ.get("CODEX_HOME")
760
- if codex_home:
761
- return Path(codex_home).expanduser() / "sessions"
762
- return Path.home() / ".codex" / "sessions"
763
-
764
- def _find_codex_child_session_file(self, thread_id):
765
- """Find a Codex transcript file for a child thread ID."""
766
- sessions_dir = self._codex_sessions_dir()
767
- if not sessions_dir.exists():
768
- return None
769
-
770
- try:
771
- matches = list(sessions_dir.rglob(f"*{thread_id}.jsonl"))
772
- except OSError:
773
- return None
774
-
775
- if not matches:
776
- return None
777
-
778
- try:
779
- matches.sort(key=lambda path: path.stat().st_mtime, reverse=True)
780
- except OSError:
781
- pass
782
- return str(matches[0])
783
-
784
- def _is_tracked_claude_subagent_state(self, state):
785
- """Return true for Claude Code task events representing in-process agents."""
786
- if not isinstance(state, dict):
787
- return False
788
- task_type = str(state.get("task_type") or "")
789
- task_type_lower = task_type.lower()
790
- subagent_type = str(state.get("subagent_type") or "")
791
- if task_type_lower == "local_bash":
792
- return False
793
- tracked_types = {"in_process_teammate", "subagent", "agent", "teammate"}
794
- if task_type_lower in tracked_types:
795
- return True
796
- if task_type_lower == "local_agent" and subagent_type:
797
- return True
798
- summary = str(state.get("summary") or "")
799
- return bool(
800
- not task_type
801
- and summary.lower().startswith(("dev:", "critic:", "reviewer:", "agent:"))
802
- )
803
-
804
- def _has_tracked_claude_subagent_task(self):
805
- """Return true once a Claude Code local-agent/subagent task has been observed."""
806
- return any(
807
- self._is_tracked_claude_subagent_state(state)
808
- for state in self.claude_task_states.values()
809
- )
810
-
811
- def _update_claude_subagent_status_counts(self):
812
- """Track Claude Code in-process teammate task state counts."""
813
- counts = Counter()
814
- active = 0
815
- inactive_statuses = {
816
- "completed",
817
- "failed",
818
- "cancelled",
819
- "canceled",
820
- "killed",
821
- "stopped",
822
- "success",
823
- "error",
824
- }
825
- for state in self.claude_task_states.values():
826
- if not self._is_tracked_claude_subagent_state(state):
827
- continue
828
- status = str(state.get("status") or "unknown")
829
- counts[status] += 1
830
- if status.lower() not in inactive_statuses:
831
- active += 1
832
- summary = state.get("summary") or state.get("subagent_type")
833
- if summary:
834
- self.last_text_snippet = str(summary).strip()[:120]
835
- self._detect_phase(str(summary))
836
- self.subagent_status_counts = counts
837
- self.active_subagent_count = active
838
-
839
- def _claude_projects_dir(self):
840
- """Return the Claude Code projects directory for transcript lookup."""
841
- projects_dir = os.environ.get("CLAUDE_PROJECTS_DIR")
842
- if projects_dir:
843
- return Path(projects_dir).expanduser()
844
- claude_config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
845
- if claude_config_dir:
846
- return Path(claude_config_dir).expanduser() / "projects"
847
- claude_home = os.environ.get("CLAUDE_HOME")
848
- if claude_home:
849
- return Path(claude_home).expanduser() / "projects"
850
- return Path.home() / ".claude" / "projects"
851
-
852
- def _claude_project_key(self):
853
- """Encode cwd the same way Claude Code stores project transcript dirs."""
854
- cwd = self.claude_cwd
855
- if not cwd:
856
- return ""
857
- return cwd.replace("\\", "-").replace("/", "-").replace(":", "")
858
-
859
- def _find_claude_child_session_files(self):
860
- """Find Claude Code subagent transcripts for this parent session."""
861
- if not self.claude_session_id:
862
- return []
863
-
864
- projects_dir = self._claude_projects_dir()
865
- if not projects_dir.exists():
866
- return []
867
-
868
- candidates = []
869
- project_key = self._claude_project_key()
870
- if project_key:
871
- candidates.append(
872
- projects_dir / project_key / self.claude_session_id / "subagents"
873
- )
874
-
875
- for candidate in candidates:
876
- if candidate.exists():
877
- try:
878
- return sorted(candidate.glob("*.jsonl"))
879
- except OSError:
880
- return []
881
-
882
- # Fallback for non-standard cwd encoding or custom Claude homes. Avoid
883
- # repeatedly walking every stored transcript before any Agent task exists.
884
- if not self._has_tracked_claude_subagent_task():
885
- return []
886
-
887
- fallback_scan_key = f"{projects_dir}:{self.claude_session_id}"
888
- now = time.monotonic()
889
- if (
890
- self._last_claude_fallback_scan_key == fallback_scan_key
891
- and now - self._last_claude_fallback_scan_at < self._claude_fallback_scan_interval_seconds
892
- ):
893
- return self._claude_child_session_files
894
- self._last_claude_fallback_scan_key = fallback_scan_key
895
- self._last_claude_fallback_scan_at = now
896
- try:
897
- matches = sorted(projects_dir.rglob(f"{self.claude_session_id}/subagents/*.jsonl"))
898
- except OSError:
899
- return []
900
- return matches
901
-
902
- def _cb_projects_dir(self):
903
- """Return the CodeBuddy projects directory for transcript lookup.
904
-
905
- CodeBuddy stores per-project session transcripts and sub-agent data
906
- under ~/.codebuddy/projects/{projectDir}/{sessionId}/.
907
- """
908
- cb_config = os.environ.get("CODEBUDDY_CONFIG_DIR")
909
- if cb_config:
910
- return Path(cb_config).expanduser() / "projects"
911
- cb_home = os.environ.get("CODEBUDDY_HOME")
912
- if cb_home:
913
- return Path(cb_home).expanduser() / "projects"
914
- return Path.home() / ".codebuddy" / "projects"
915
-
916
- def _cb_project_key(self):
917
- """Encode cwd the same way CodeBuddy stores project subdirs.
918
-
919
- CodeBuddy uses the same sanitisation as Claude Code (\, /, : → -)
920
- but strips the leading '-' so "/Users/test/MyProject" becomes
921
- "Users-test-MyProject".
922
- """
923
- cwd = self.cb_cwd
924
- if not cwd:
925
- return ""
926
- return cwd.replace("\\", "-").replace("/", "-").replace(":", "").lstrip("-")
927
-
928
- def _find_cb_child_session_files(self):
929
- """Find CodeBuddy subagent transcript data for this parent session.
930
-
931
- CodeBuddy sub-agents store tool-results/{callId}.txt files; conversation
932
- transcripts are not (as of 2026) written as agent-*.jsonl files in the
933
- subagents/ directory. We track the tool-results .txt files as a proxy
934
- for child activity so the heartbeat monitor can extend the stale-kill
935
- window while sub-agents are running.
936
- """
937
- if not self.cb_session_id:
938
- return []
939
-
940
- projects_dir = self._cb_projects_dir()
941
- if not projects_dir.exists():
942
- return []
943
-
944
- project_key = self._cb_project_key()
945
- if not project_key:
946
- return []
947
-
948
- subagents_dir = (
949
- projects_dir / project_key / self.cb_session_id / "subagents"
950
- )
951
- if not subagents_dir.exists():
952
- return []
953
-
954
- # Collect all files under each agent-{id} subdirectory. These are
955
- # currently tool-results/{callId}.txt files, but the glob also picks
956
- # up future agent-*.jsonl transcripts should CodeBuddy add them.
957
- try:
958
- return sorted(
959
- p for p in subagents_dir.glob("**/*")
960
- if p.is_file()
961
- )
962
- except OSError:
963
- return []
964
-
965
- def refresh_child_session_activity(self, force=False):
966
- """Refresh child transcript file stats.
967
-
968
- The heartbeat monitor uses this activity signature to treat subagent
969
- transcript growth as real progress while the parent session is blocked
970
- waiting for a child agent/tool result. Supports Codex child threads,
971
- Claude Code in-process teammate transcripts, and CodeBuddy sub-agent
972
- file activity under subagents/.
973
- """
974
- previous_signature = self.child_activity_signature
975
-
976
- if (
977
- not self.codex_child_thread_ids
978
- and not self.claude_session_id
979
- and not self.cb_session_id
980
- ):
981
- self.child_session_files = []
982
- self.child_total_bytes = 0
983
- self.child_activity_signature = ""
984
- self.last_child_activity_at = ""
985
- return previous_signature != self.child_activity_signature
986
-
987
- now = time.monotonic()
988
- should_scan = (
989
- force
990
- or self._last_child_scan_at == 0.0
991
- or (now - self._last_child_scan_at >= 2.0)
992
- )
993
- if should_scan:
994
- for thread_id in sorted(self.codex_child_thread_ids):
995
- path = self._codex_child_session_paths.get(thread_id)
996
- if not path or not os.path.exists(path):
997
- found = self._find_codex_child_session_file(thread_id)
998
- if found:
999
- self._codex_child_session_paths[thread_id] = found
1000
- self._claude_child_session_files = self._find_claude_child_session_files()
1001
- self._cb_child_session_files = self._find_cb_child_session_files()
1002
- self._last_child_scan_at = now
1003
-
1004
- files = []
1005
- signature_parts = []
1006
- total_bytes = 0
1007
- max_mtime = 0.0
1008
-
1009
- def add_file(kind, identifier, path):
1010
- nonlocal total_bytes, max_mtime
1011
- try:
1012
- stat = os.stat(path)
1013
- except OSError:
1014
- return
1015
- path_str = str(path)
1016
- total_bytes += stat.st_size
1017
- max_mtime = max(max_mtime, stat.st_mtime)
1018
- mtime_ns = getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000))
1019
- signature_parts.append(f"{kind}:{identifier}:{stat.st_size}:{mtime_ns}")
1020
- files.append(
1021
- {
1022
- "kind": kind,
1023
- "thread_id": identifier,
1024
- "path": path_str,
1025
- "size": stat.st_size,
1026
- "mtime": datetime.fromtimestamp(
1027
- stat.st_mtime, timezone.utc
1028
- ).strftime("%Y-%m-%dT%H:%M:%SZ"),
1029
- }
1030
- )
1031
-
1032
- for thread_id in sorted(self.codex_child_thread_ids):
1033
- path = self._codex_child_session_paths.get(thread_id)
1034
- if path:
1035
- add_file("codex", thread_id, path)
1036
-
1037
- for path in self._claude_child_session_files:
1038
- add_file("claude", path.stem, path)
1039
-
1040
- for path in self._cb_child_session_files:
1041
- # Identifier for CodeBuddy sub-agent files: use the parent
1042
- # directory name (agent-{id}) so heartbeat can distinguish
1043
- # activity across different sub-agent instances.
1044
- parent_name = path.parent.name if hasattr(path, "parent") else ""
1045
- identifier = parent_name if parent_name.startswith("agent-") else path.name
1046
- add_file("codebuddy", identifier, path)
1047
-
1048
- self.child_session_files = files
1049
- self.child_total_bytes = total_bytes
1050
- self.child_activity_signature = "|".join(signature_parts)
1051
- self.last_child_activity_at = (
1052
- datetime.fromtimestamp(max_mtime, timezone.utc).strftime(
1053
- "%Y-%m-%dT%H:%M:%SZ"
1054
- )
1055
- if max_mtime
1056
- else ""
1057
- )
1058
- return previous_signature != self.child_activity_signature
1059
-
1060
- def to_dict(self):
1061
- """Export current state as a dictionary for JSON serialization."""
1062
- self.refresh_child_session_activity()
1063
- tool_calls = [
1064
- {"name": name, "count": count}
1065
- for name, count in self.tool_call_counts.most_common()
1066
- ]
1067
- subagent_states = [
1068
- {"status": status, "count": count}
1069
- for status, count in self.subagent_status_counts.most_common()
1070
- ]
1071
- return {
1072
- "updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
1073
- "event_format": self.event_format,
1074
- "message_count": self.message_count,
1075
- "current_tool": self.current_tool,
1076
- "current_tool_input_summary": self.current_tool_input_summary,
1077
- "current_phase": self.current_phase,
1078
- "detected_phases": self.detected_phases,
1079
- "tool_calls": tool_calls,
1080
- "total_tool_calls": self.total_tool_calls,
1081
- "active_subagent_count": self.active_subagent_count,
1082
- "subagent_states": subagent_states,
1083
- "subagent_spawn_count": self._subagent_spawn_count,
1084
- "child_thread_ids": sorted(self.codex_child_thread_ids),
1085
- "child_session_files": self.child_session_files,
1086
- "child_total_bytes": self.child_total_bytes,
1087
- "child_activity_signature": self.child_activity_signature,
1088
- "last_child_activity_at": self.last_child_activity_at,
1089
- "cb_session_id": self.cb_session_id,
1090
- "cb_cwd": self.cb_cwd,
1091
- "last_text_snippet": self.last_text_snippet,
1092
- "last_result_is_error": self.last_result_is_error,
1093
- "api_error_status": self.api_error_status,
1094
- "api_error_code": self.api_error_code,
1095
- "terminal_result_text": self.terminal_result_text,
1096
- "terminal_success_at": self.terminal_success_at,
1097
- "fatal_error_code": self.fatal_error_code,
1098
- "is_active": self.is_active,
1099
- "errors": self.errors[-10:], # Keep last 10 errors
1100
- }
1101
-
1102
-
1103
- def atomic_write_json(data, filepath):
1104
- """Write JSON data atomically using tmp file + rename."""
1105
- dir_path = os.path.dirname(filepath)
1106
- tmp_path = None
1107
- try:
1108
- fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")
1109
- with os.fdopen(fd, "w") as f:
1110
- json.dump(data, f, indent=2)
1111
- f.write("\n")
1112
- os.rename(tmp_path, filepath)
1113
- except OSError:
1114
- # Best effort - remove tmp file if rename failed
1115
- if tmp_path:
1116
- try:
1117
- os.unlink(tmp_path)
1118
- except OSError:
1119
- pass
1120
-
1121
-
1122
- def tail_and_parse(session_log, progress_file, poll_interval=0.5):
1123
- """Tail session log and parse stream-json events."""
1124
- tracker = ProgressTracker(session_log)
1125
- last_write_state = None
1126
-
1127
- def state_key(state):
1128
- return (
1129
- state["message_count"],
1130
- state["current_tool"],
1131
- state["current_phase"],
1132
- state["total_tool_calls"],
1133
- state.get("child_activity_signature", ""),
1134
- state.get("last_result_is_error"),
1135
- state.get("api_error_status"),
1136
- state.get("api_error_code", ""),
1137
- state.get("fatal_error_code", ""),
1138
- state.get("terminal_result_text", ""),
1139
- tuple(state.get("errors", [])),
1140
- )
1141
-
1142
- # Wait for log file to appear
1143
- wait_count = 0
1144
- while not os.path.exists(session_log):
1145
- time.sleep(poll_interval)
1146
- wait_count += 1
1147
- if wait_count > 120: # 60 seconds max wait
1148
- sys.stderr.write(f"Timeout waiting for {session_log}\n")
1149
- sys.exit(1)
1150
-
1151
- with open(session_log, "r") as f:
1152
- idle_count = 0
1153
- while True:
1154
- line = f.readline()
1155
- if line:
1156
- idle_count = 0
1157
- line = line.strip()
1158
- if not line:
1159
- continue
1160
- try:
1161
- event = json.loads(line)
1162
- tracker.process_event(event)
1163
- except json.JSONDecodeError:
1164
- # Not a JSON line (could be stderr mixed in). Use it as a
1165
- # text snippet and only treat it as terminal when it has a
1166
- # strong API/runtime error marker; ordinary assistant prose
1167
- # can discuss context limits without being fatal.
1168
- stripped = line.strip()
1169
- if stripped and len(stripped) > 5:
1170
- tracker.last_text_snippet = stripped[:120]
1171
- tracker._detect_terminal_error(stripped, require_error_context=True)
1172
- current_state = tracker.to_dict()
1173
- current_state_key = state_key(current_state)
1174
- if current_state_key != last_write_state:
1175
- atomic_write_json(current_state, progress_file)
1176
- last_write_state = current_state_key
1177
- continue
1178
-
1179
- # Write progress if state changed
1180
- current_state = tracker.to_dict()
1181
- current_state_key = state_key(current_state)
1182
- if current_state_key != last_write_state:
1183
- atomic_write_json(current_state, progress_file)
1184
- last_write_state = current_state_key
1185
- else:
1186
- idle_count += 1
1187
- # Every 2 seconds of no parent log data, refresh child Codex
1188
- # transcript stats and write if child activity advanced.
1189
- if idle_count % 4 == 0:
1190
- current_state = tracker.to_dict()
1191
- current_state_key = state_key(current_state)
1192
- if current_state_key != last_write_state or idle_count == 4:
1193
- atomic_write_json(current_state, progress_file)
1194
- last_write_state = current_state_key
1195
-
1196
- # After 3600 idle cycles (30 min), mark inactive and exit
1197
- if idle_count > 3600:
1198
- tracker.is_active = False
1199
- atomic_write_json(tracker.to_dict(), progress_file)
1200
- break
1201
-
1202
- time.sleep(poll_interval)
1203
-
1204
- # Final write
1205
- tracker.is_active = False
1206
- atomic_write_json(tracker.to_dict(), progress_file)
1207
-
1208
-
1209
- def main():
1210
- parser = argparse.ArgumentParser(description="Parse stream-json progress")
1211
- parser.add_argument("--session-log", required=True, help="Path to session.log (JSONL)")
1212
- parser.add_argument("--progress-file", required=True, help="Path to write progress.json")
1213
- args = parser.parse_args()
1214
-
1215
- # Handle graceful shutdown
1216
- def handle_signal(signum, frame):
1217
- sys.exit(0)
1218
-
1219
- signal.signal(signal.SIGTERM, handle_signal)
1220
- signal.signal(signal.SIGINT, handle_signal)
1221
-
1222
- try:
1223
- tail_and_parse(args.session_log, args.progress_file)
1224
- except SystemExit:
1225
- pass
1226
- except Exception as e:
1227
- sys.stderr.write(f"parse-stream-progress.py error: {e}\n")
1228
- sys.exit(1)
1229
-
1230
-
1231
- if __name__ == "__main__":
1232
- main()