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.
- package/bin/create-prizmkit.js +4 -4
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/.env.example +3 -3
- package/bundled/dev-pipeline/README.md +110 -90
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
- package/bundled/dev-pipeline/cli.py +27 -0
- package/bundled/dev-pipeline/prizmkit_runtime/__init__.py +13 -0
- package/bundled/dev-pipeline/prizmkit_runtime/__main__.py +9 -0
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +193 -0
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +313 -0
- package/bundled/dev-pipeline/prizmkit_runtime/compat.py +32 -0
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +410 -0
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +478 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +734 -0
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +256 -0
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +109 -0
- package/bundled/dev-pipeline/prizmkit_runtime/paths.py +110 -0
- package/bundled/dev-pipeline/prizmkit_runtime/processes.py +373 -0
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +389 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +51 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +179 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +345 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +118 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +187 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +147 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +422 -0
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +552 -0
- package/bundled/dev-pipeline/prizmkit_runtime/status.py +71 -0
- package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
- package/bundled/dev-pipeline/scripts/continuation.py +374 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +64 -52
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +35 -25
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +2 -2
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +185 -16
- package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
- package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +8 -2
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +4 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -5
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +16 -6
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +16 -6
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -0
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +13 -69
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +15 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +42 -29
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +41 -39
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +4 -0
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +4 -0
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_auto_skip.py +932 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +100 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +1138 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +1045 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +29 -34
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +6 -5
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +29 -35
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +7 -6
- package/bundled/skills/feature-workflow/SKILL.md +3 -6
- package/bundled/skills/recovery-workflow/SKILL.md +23 -32
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +29 -35
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +7 -6
- package/bundled/skills/refactor-workflow/SKILL.md +3 -6
- package/bundled/templates/hooks/commit-intent.json +2 -2
- package/bundled/templates/hooks/diff-prizm-docs.py +226 -0
- package/bundled/templates/hooks/prizm-pre-commit.py +45 -0
- package/bundled/templates/hooks/validate-prizm-docs.py +240 -0
- package/package.json +1 -1
- package/src/config.js +33 -12
- package/src/index.js +3 -6
- package/src/manifest.js +3 -2
- package/src/metadata.js +7 -18
- package/src/prompts.js +0 -12
- package/src/runtimes.js +18 -11
- package/src/scaffold.js +133 -116
- package/src/upgrade.js +1 -1
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -500
- package/bundled/dev-pipeline/launch-feature-daemon.sh +0 -657
- package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -502
- package/bundled/dev-pipeline/lib/branch.sh +0 -326
- package/bundled/dev-pipeline/lib/common.sh +0 -1257
- package/bundled/dev-pipeline/lib/heartbeat.sh +0 -469
- package/bundled/dev-pipeline/reset-bug.sh +0 -395
- package/bundled/dev-pipeline/reset-feature.sh +0 -406
- package/bundled/dev-pipeline/reset-refactor.sh +0 -393
- package/bundled/dev-pipeline/run-bugfix.sh +0 -1315
- package/bundled/dev-pipeline/run-feature.sh +0 -1602
- package/bundled/dev-pipeline/run-recovery.sh +0 -648
- package/bundled/dev-pipeline/run-refactor.sh +0 -1348
- package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
- package/bundled/dev-pipeline/tests/test-path-resolution.sh +0 -100
- package/bundled/dev-pipeline-windows/.env.example +0 -36
- package/bundled/dev-pipeline-windows/README.md +0 -30
- package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -533
- package/bundled/dev-pipeline-windows/assets/feature-list-example.json +0 -146
- package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -137
- package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +0 -9
- package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +0 -9
- package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +0 -9
- package/bundled/dev-pipeline-windows/lib/branch.ps1 +0 -235
- package/bundled/dev-pipeline-windows/lib/common.ps1 +0 -829
- package/bundled/dev-pipeline-windows/lib/daemon.ps1 +0 -140
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +0 -967
- package/bundled/dev-pipeline-windows/lib/reset.ps1 +0 -87
- package/bundled/dev-pipeline-windows/reset-bug.ps1 +0 -9
- package/bundled/dev-pipeline-windows/reset-feature.ps1 +0 -9
- package/bundled/dev-pipeline-windows/reset-refactor.ps1 +0 -9
- package/bundled/dev-pipeline-windows/run-bugfix.ps1 +0 -9
- package/bundled/dev-pipeline-windows/run-feature.ps1 +0 -9
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +0 -179
- package/bundled/dev-pipeline-windows/run-refactor.ps1 +0 -9
- package/bundled/dev-pipeline-windows/scripts/check-session-status.py +0 -228
- package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +0 -530
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +0 -2282
- package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +0 -767
- package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +0 -805
- package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +0 -845
- package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +0 -316
- package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +0 -134
- package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +0 -382
- package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +0 -399
- package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +0 -102
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +0 -1232
- package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +0 -191
- package/bundled/dev-pipeline-windows/scripts/prizmkit-test-gate.py +0 -446
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +0 -913
- package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +0 -173
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +0 -1558
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +0 -1119
- package/bundled/dev-pipeline-windows/scripts/utils.py +0 -542
- package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +0 -7
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +0 -65
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +0 -5
- package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +0 -7
- package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +0 -73
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +0 -589
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +0 -710
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +0 -791
- package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +0 -263
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +0 -326
- package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +0 -237
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +0 -340
- package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +0 -270
- package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +0 -7
- package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +0 -91
- package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +0 -34
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +0 -10
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +0 -12
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +0 -7
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +0 -8
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +0 -9
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +0 -6
- package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +0 -21
- package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +0 -21
- package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +0 -78
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +0 -283
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +0 -112
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +0 -168
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +0 -82
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +0 -75
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +0 -23
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +0 -14
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +0 -19
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +0 -63
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +0 -42
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +0 -44
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +0 -59
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +0 -39
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +0 -27
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +0 -27
- package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +0 -39
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +0 -35
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +0 -35
- package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +0 -73
- package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +0 -13
- package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +0 -21
- package/bundled/dev-pipeline-windows/templates/sections/session-context.md +0 -5
- package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +0 -6
- package/bundled/dev-pipeline-windows/templates/sections/task-contract.md +0 -34
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +0 -48
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +0 -48
- package/bundled/dev-pipeline-windows/templates/session-status-schema.json +0 -83
- package/bundled/skills-windows/app-planner/SKILL.md +0 -296
- package/bundled/skills-windows/app-planner/assets/app-design-guide.md +0 -101
- package/bundled/skills-windows/app-planner/references/architecture-decisions.md +0 -52
- package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +0 -101
- package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +0 -71
- package/bundled/skills-windows/app-planner/references/infrastructure-convention-discovery.md +0 -108
- package/bundled/skills-windows/app-planner/references/project-brief-guide.md +0 -82
- package/bundled/skills-windows/app-planner/references/project-conventions-discovery.md +0 -59
- package/bundled/skills-windows/app-planner/references/project-state-detection.md +0 -90
- package/bundled/skills-windows/app-planner/references/red-team-checklist.md +0 -40
- package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +0 -619
- package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +0 -285
- package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +0 -249
- package/bundled/skills-windows/app-planner/references/rules/backend/question-manifest.json +0 -46
- package/bundled/skills-windows/app-planner/references/rules/backend/template.md +0 -173
- package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +0 -382
- package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +0 -211
- package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +0 -184
- package/bundled/skills-windows/app-planner/references/rules/database/question-manifest.json +0 -39
- package/bundled/skills-windows/app-planner/references/rules/database/template.md +0 -158
- package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +0 -820
- package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +0 -188
- package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +0 -319
- package/bundled/skills-windows/app-planner/references/rules/frontend/question-manifest.json +0 -51
- package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +0 -339
- package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +0 -649
- package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +0 -290
- package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +0 -232
- package/bundled/skills-windows/app-planner/references/rules/mobile/question-manifest.json +0 -47
- package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +0 -175
- package/bundled/skills-windows/app-planner/references/rules-configuration.md +0 -46
- package/bundled/skills-windows/bug-fix-workflow/SKILL.md +0 -379
- package/bundled/skills-windows/bug-fix-workflow/references/bug-diagnosis.md +0 -41
- package/bundled/skills-windows/bug-planner/SKILL.md +0 -402
- package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +0 -43
- package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +0 -44
- package/bundled/skills-windows/bug-planner/references/error-recovery.md +0 -73
- package/bundled/skills-windows/bug-planner/references/input-formats.md +0 -53
- package/bundled/skills-windows/bug-planner/references/schema-validation.md +0 -25
- package/bundled/skills-windows/bug-planner/references/severity-rules.md +0 -16
- package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +0 -322
- package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +0 -304
- package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +0 -84
- package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +0 -381
- package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +0 -67
- package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
- package/bundled/skills-windows/feature-planner/SKILL.md +0 -397
- package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +0 -64
- package/bundled/skills-windows/feature-planner/assets/planning-guide.md +0 -214
- package/bundled/skills-windows/feature-planner/references/browser-interaction.md +0 -59
- package/bundled/skills-windows/feature-planner/references/completeness-review.md +0 -57
- package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +0 -75
- package/bundled/skills-windows/feature-planner/references/error-recovery.md +0 -90
- package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +0 -112
- package/bundled/skills-windows/feature-planner/references/new-project-planning.md +0 -85
- package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +0 -1020
- package/bundled/skills-windows/feature-workflow/SKILL.md +0 -374
- package/bundled/skills-windows/feature-workflow/references/brainstorm-guide.md +0 -137
- package/bundled/skills-windows/prizm-kit/SKILL.md +0 -93
- package/bundled/skills-windows/prizmkit-code-review/SKILL.md +0 -161
- package/bundled/skills-windows/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
- package/bundled/skills-windows/prizmkit-code-review/references/review-report-template.md +0 -31
- package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +0 -66
- package/bundled/skills-windows/prizmkit-code-review/scripts/check_loop.py +0 -186
- package/bundled/skills-windows/prizmkit-committer/SKILL.md +0 -89
- package/bundled/skills-windows/prizmkit-deploy/SKILL.md +0 -444
- package/bundled/skills-windows/prizmkit-deploy/references/ci-cd-workflows.md +0 -115
- package/bundled/skills-windows/prizmkit-deploy/references/cloud-platform-deploy.md +0 -93
- package/bundled/skills-windows/prizmkit-deploy/references/data-safety-examples.md +0 -120
- package/bundled/skills-windows/prizmkit-deploy/references/database-setup.md +0 -46
- package/bundled/skills-windows/prizmkit-deploy/references/deploy-config-schema.md +0 -148
- package/bundled/skills-windows/prizmkit-deploy/references/deploy-history-schema.md +0 -62
- package/bundled/skills-windows/prizmkit-deploy/references/deployment-modes.md +0 -50
- package/bundled/skills-windows/prizmkit-deploy/references/direct-upload.md +0 -26
- package/bundled/skills-windows/prizmkit-deploy/references/dns-setup.md +0 -42
- package/bundled/skills-windows/prizmkit-deploy/references/docker-deploy.md +0 -31
- package/bundled/skills-windows/prizmkit-deploy/references/firewall-setup.md +0 -37
- package/bundled/skills-windows/prizmkit-deploy/references/live-validation-notes.md +0 -21
- package/bundled/skills-windows/prizmkit-deploy/references/nginx-blue-green.md +0 -59
- package/bundled/skills-windows/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
- package/bundled/skills-windows/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
- package/bundled/skills-windows/prizmkit-deploy/references/ssh-takeover.md +0 -20
- package/bundled/skills-windows/prizmkit-deploy/references/ssl-setup.md +0 -56
- package/bundled/skills-windows/prizmkit-implement/SKILL.md +0 -71
- package/bundled/skills-windows/prizmkit-init/SKILL.md +0 -356
- package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +0 -82
- package/bundled/skills-windows/prizmkit-init/references/config-schema.md +0 -70
- package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +0 -41
- package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +0 -13
- package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +0 -9
- package/bundled/skills-windows/prizmkit-plan/SKILL.md +0 -102
- package/bundled/skills-windows/prizmkit-plan/assets/plan-template.md +0 -115
- package/bundled/skills-windows/prizmkit-plan/assets/spec-template.md +0 -73
- package/bundled/skills-windows/prizmkit-plan/references/clarify-guide.md +0 -67
- package/bundled/skills-windows/prizmkit-plan/references/examples.md +0 -85
- package/bundled/skills-windows/prizmkit-plan/references/verification-checklist.md +0 -60
- package/bundled/skills-windows/prizmkit-prizm-docs/SKILL.md +0 -129
- package/bundled/skills-windows/prizmkit-prizm-docs/assets/prizm-docs-format.md +0 -613
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-init.md +0 -45
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-rebuild.md +0 -15
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-status.md +0 -14
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-update.md +0 -19
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-validate.md +0 -17
- package/bundled/skills-windows/prizmkit-retrospective/SKILL.md +0 -87
- package/bundled/skills-windows/prizmkit-retrospective/references/knowledge-injection-steps.md +0 -50
- package/bundled/skills-windows/prizmkit-retrospective/references/structural-sync-steps.md +0 -43
- package/bundled/skills-windows/prizmkit-test/SKILL.md +0 -227
- package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +0 -220
- package/bundled/skills-windows/prizmkit-test/references/examples.md +0 -143
- package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +0 -225
- package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +0 -109
- package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +0 -111
- package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +0 -347
- package/bundled/skills-windows/recovery-workflow/SKILL.md +0 -355
- package/bundled/skills-windows/recovery-workflow/evals/evals.json +0 -46
- package/bundled/skills-windows/recovery-workflow/references/detection.md +0 -58
- package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +0 -544
- package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +0 -346
- package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +0 -72
- package/bundled/skills-windows/refactor-planner/SKILL.md +0 -398
- package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +0 -292
- package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +0 -301
- package/bundled/skills-windows/refactor-planner/references/fast-path.md +0 -59
- package/bundled/skills-windows/refactor-planner/references/planning-phases.md +0 -135
- package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +0 -221
- package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +0 -858
- package/bundled/skills-windows/refactor-workflow/SKILL.md +0 -343
- package/bundled/skills-windows/refactor-workflow/references/brainstorm-guide.md +0 -116
- package/bundled/templates/hooks/diff-prizm-docs.sh +0 -158
- package/bundled/templates/hooks/prizm-pre-commit.sh +0 -108
- 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()
|