cctx-cli 1.6.0__tar.gz → 1.7.0__tar.gz
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.
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/CHANGELOG.md +41 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/PKG-INFO +1 -1
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/__init__.py +54 -8
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/exporters/jsonl.py +10 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/models.py +12 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/renderers/templates/autopsy.html.j2 +15 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/renderers/terminal.py +22 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/pyproject.toml +1 -1
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/exporters/test_jsonl.py +37 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/renderers/test_report.py +23 -0
- cctx_cli-1.7.0/tests/test_diagnostician_subagents.py +298 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/.gitignore +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/CLAUDE.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/DESIGN.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/PRODUCT.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/README.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/action.yml +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/agents.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/cli.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/harvest.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/recommender/claude_md.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/demo.gif +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/demo.tape +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/conftest.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_harvest_emit.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_models.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.6.0 → cctx_cli-1.7.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.7.0 (2026-06-11)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Jsonl exporter — subagent_costs in dict literal; fix import sort
|
|
10
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
11
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
12
|
+
|
|
13
|
+
- Remove unused ToolResult import in test_diagnostician_subagents
|
|
14
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
15
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
- Restore billing-rate explanation in _compute_own_cost
|
|
20
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
21
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
- Diagnostician — inclusive cost + per-subagent attribution
|
|
26
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
27
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
28
|
+
|
|
29
|
+
- HTML report + JSON exporter — subagent_costs output
|
|
30
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
31
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
32
|
+
|
|
33
|
+
- Per-subagent cost attribution in autopsy (#88)
|
|
34
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
35
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
36
|
+
|
|
37
|
+
- SubagentAttribution model + Diagnosis.subagent_costs field
|
|
38
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
39
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
40
|
+
|
|
41
|
+
- Terminal renderer — subagent cost table in autopsy output
|
|
42
|
+
([#109](https://github.com/jacquardlabs/cctx/pull/109),
|
|
43
|
+
[`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
|
|
44
|
+
|
|
45
|
+
|
|
5
46
|
## v1.6.0 (2026-06-10)
|
|
6
47
|
|
|
7
48
|
### Bug Fixes
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Autopsy diagnostician — public entry point.
|
|
2
2
|
|
|
3
3
|
run(trace) -> Diagnosis
|
|
4
|
-
Runs all
|
|
5
|
-
patches cost attribution
|
|
6
|
-
a Diagnosis with patches=[].
|
|
4
|
+
Runs all pattern classifiers, detects inflection turn,
|
|
5
|
+
patches stale_context cost attribution, and returns
|
|
6
|
+
a Diagnosis with patches=[] and subagent_costs populated.
|
|
7
7
|
|
|
8
8
|
The Recommender (cctx.recommender.claude_md) populates patches.
|
|
9
9
|
"""
|
|
@@ -21,7 +21,7 @@ from cctx.diagnostician.patterns import (
|
|
|
21
21
|
stale_context,
|
|
22
22
|
tool_thrash,
|
|
23
23
|
)
|
|
24
|
-
from cctx.models import Diagnosis, Finding, FindingKind
|
|
24
|
+
from cctx.models import Diagnosis, Finding, FindingKind, SubagentAttribution
|
|
25
25
|
from cctx.pricing import price_per_tok as _price_per_tok
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
@@ -41,8 +41,8 @@ def _patch_costs(findings: list[Finding], model: str | None) -> list[Finding]:
|
|
|
41
41
|
return result
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
"""
|
|
44
|
+
def _compute_own_cost(trace: SessionTrace, model: str | None) -> float:
|
|
45
|
+
"""Parent-turns-only cost — does not recurse into subagents.
|
|
46
46
|
|
|
47
47
|
Billing rates relative to base input price:
|
|
48
48
|
cache_read: ×0.10 (read from prompt cache)
|
|
@@ -59,6 +59,50 @@ def _compute_total_cost(trace: SessionTrace, model: str | None) -> float:
|
|
|
59
59
|
return round(total, 4)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _compute_inclusive_cost(trace: SessionTrace) -> float:
|
|
63
|
+
"""Recursive cost: own turns + all subagent turns at every depth."""
|
|
64
|
+
own = _compute_own_cost(trace, trace.primary_model)
|
|
65
|
+
return own + sum(_compute_inclusive_cost(sa) for sa in trace.subagents)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_label_map(trace: SessionTrace) -> dict[str, str]:
|
|
69
|
+
"""Map child session_id → display label from the parent's Agent ToolUse inputs."""
|
|
70
|
+
label_map: dict[str, str] = {}
|
|
71
|
+
for turn in trace.turns:
|
|
72
|
+
for tu in turn.tool_uses:
|
|
73
|
+
if tu.subagent_session_id:
|
|
74
|
+
ti = tu.tool_input
|
|
75
|
+
label_map[tu.subagent_session_id] = (
|
|
76
|
+
ti.get("description")
|
|
77
|
+
or (ti.get("prompt") or "")[:80]
|
|
78
|
+
or tu.subagent_session_id[:12]
|
|
79
|
+
)
|
|
80
|
+
return label_map
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _collect_attributions(
|
|
84
|
+
trace: SessionTrace,
|
|
85
|
+
depth: int = 1,
|
|
86
|
+
label_map: dict[str, str] | None = None,
|
|
87
|
+
) -> list[SubagentAttribution]:
|
|
88
|
+
"""Flat DFS list of SubagentAttribution, one per subagent at every depth."""
|
|
89
|
+
if label_map is None:
|
|
90
|
+
label_map = _build_label_map(trace)
|
|
91
|
+
result: list[SubagentAttribution] = []
|
|
92
|
+
for child in trace.subagents:
|
|
93
|
+
label = label_map.get(child.session_id, child.session_id[:12])
|
|
94
|
+
cost = _compute_inclusive_cost(child)
|
|
95
|
+
result.append(SubagentAttribution(
|
|
96
|
+
session_id=child.session_id,
|
|
97
|
+
label=label,
|
|
98
|
+
total_cost_usd=round(cost, 4),
|
|
99
|
+
depth=depth,
|
|
100
|
+
model=child.primary_model,
|
|
101
|
+
))
|
|
102
|
+
result.extend(_collect_attributions(child, depth + 1, None))
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
62
106
|
def run(trace: SessionTrace) -> Diagnosis:
|
|
63
107
|
"""Diagnose a single SessionTrace. Returns Diagnosis with patches=[]."""
|
|
64
108
|
findings: list[Finding] = [
|
|
@@ -73,11 +117,12 @@ def run(trace: SessionTrace) -> Diagnosis:
|
|
|
73
117
|
inflection_turn = inflection.detect(findings)
|
|
74
118
|
findings = _patch_costs(findings, trace.primary_model)
|
|
75
119
|
|
|
76
|
-
total_cost =
|
|
120
|
+
total_cost = round(_compute_inclusive_cost(trace), 4)
|
|
77
121
|
waste_cost = sum(f.cost_usd for f in findings if f.cost_usd is not None)
|
|
78
|
-
# Waste cannot exceed total session cost — cap as a logical invariant.
|
|
79
122
|
waste_cost = min(waste_cost, total_cost)
|
|
80
123
|
|
|
124
|
+
subagent_costs = _collect_attributions(trace)
|
|
125
|
+
|
|
81
126
|
return Diagnosis(
|
|
82
127
|
session_id=trace.session_id,
|
|
83
128
|
findings=findings,
|
|
@@ -86,4 +131,5 @@ def run(trace: SessionTrace) -> Diagnosis:
|
|
|
86
131
|
total_cost_usd=total_cost,
|
|
87
132
|
waste_cost_usd=round(waste_cost, 4),
|
|
88
133
|
analysed_at=datetime.now(UTC),
|
|
134
|
+
subagent_costs=subagent_costs,
|
|
89
135
|
)
|
|
@@ -50,6 +50,16 @@ def export_diagnosis(
|
|
|
50
50
|
"patches": patches,
|
|
51
51
|
"turn_count": len(trace.turns),
|
|
52
52
|
"model": trace.primary_model,
|
|
53
|
+
"subagent_costs": [
|
|
54
|
+
{
|
|
55
|
+
"session_id": a.session_id,
|
|
56
|
+
"label": a.label,
|
|
57
|
+
"cost_usd": a.total_cost_usd,
|
|
58
|
+
"depth": a.depth,
|
|
59
|
+
"model": a.model,
|
|
60
|
+
}
|
|
61
|
+
for a in diagnosis.subagent_costs
|
|
62
|
+
],
|
|
53
63
|
}
|
|
54
64
|
return json.dumps(obj)
|
|
55
65
|
|
|
@@ -233,6 +233,17 @@ class Patch:
|
|
|
233
233
|
evidence_summary: str
|
|
234
234
|
|
|
235
235
|
|
|
236
|
+
@dataclass
|
|
237
|
+
class SubagentAttribution:
|
|
238
|
+
"""Cost attribution for a single subagent session."""
|
|
239
|
+
|
|
240
|
+
session_id: str
|
|
241
|
+
label: str # from Agent tool_input['description'], else prompt[:80]
|
|
242
|
+
total_cost_usd: float # inclusive: this subagent + its own children
|
|
243
|
+
depth: int # 1 = direct child, 2 = grandchild, …
|
|
244
|
+
model: str | None
|
|
245
|
+
|
|
246
|
+
|
|
236
247
|
@dataclass
|
|
237
248
|
class Diagnosis:
|
|
238
249
|
session_id: str
|
|
@@ -242,6 +253,7 @@ class Diagnosis:
|
|
|
242
253
|
total_cost_usd: float
|
|
243
254
|
waste_cost_usd: float
|
|
244
255
|
analysed_at: datetime
|
|
256
|
+
subagent_costs: list[SubagentAttribution] = field(default_factory=list)
|
|
245
257
|
|
|
246
258
|
@property
|
|
247
259
|
def verdict(self) -> str:
|
|
@@ -176,6 +176,21 @@ footer {
|
|
|
176
176
|
<dt>Waste attributed</dt><dd>~${{ "%.2f"|format(diag.waste_cost_usd) }}</dd>
|
|
177
177
|
{% if diag.inflection_turn is not none %}<dt>Inflection turn</dt><dd>{{ diag.inflection_turn }}</dd>{% endif %}
|
|
178
178
|
</dl>
|
|
179
|
+
{% if diag.subagent_costs %}
|
|
180
|
+
<details class="subagent-costs">
|
|
181
|
+
<summary>Subagents: {{ diag.subagent_costs | selectattr("depth", "equalto", 1) | list | length }} — ${{ "%.3f" % (diag.subagent_costs | selectattr("depth", "equalto", 1) | map(attribute="total_cost_usd") | sum) }}</summary>
|
|
182
|
+
<table>
|
|
183
|
+
<tr><th>Label</th><th>Depth</th><th>Cost</th></tr>
|
|
184
|
+
{% for a in diag.subagent_costs %}
|
|
185
|
+
<tr>
|
|
186
|
+
<td>{{ a.label | truncate(80) }}</td>
|
|
187
|
+
<td>{{ a.depth }}</td>
|
|
188
|
+
<td>${{ "%.3f" % a.total_cost_usd }}</td>
|
|
189
|
+
</tr>
|
|
190
|
+
{% endfor %}
|
|
191
|
+
</table>
|
|
192
|
+
</details>
|
|
193
|
+
{% endif %}
|
|
179
194
|
<p class="meta">Costs are estimates (~85–95% of actual billing; system framing not observable in JSONL)</p>
|
|
180
195
|
<p class="meta">Analysed {{ diag.analysed_at.strftime("%Y-%m-%d %H:%M UTC") }}</p>
|
|
181
196
|
</div>
|
|
@@ -52,7 +52,13 @@ def render_diagnosis(
|
|
|
52
52
|
verdict = diagnosis.verdict
|
|
53
53
|
verdict_style = "bold green" if not diagnosis.findings else "bold red"
|
|
54
54
|
con.print(Text(f"Verdict: {verdict}", style=verdict_style))
|
|
55
|
+
subagent_sum = sum(a.total_cost_usd for a in diagnosis.subagent_costs if a.depth == 1)
|
|
56
|
+
n_sub = len([a for a in diagnosis.subagent_costs if a.depth == 1])
|
|
55
57
|
cost_line = f"Session cost: ~${diagnosis.total_cost_usd:.2f}"
|
|
58
|
+
if n_sub:
|
|
59
|
+
cost_line += (
|
|
60
|
+
f" (includes {n_sub} subagent{'s' if n_sub != 1 else ''}: ~${subagent_sum:.2f})"
|
|
61
|
+
)
|
|
56
62
|
if diagnosis.waste_cost_usd > 0:
|
|
57
63
|
pct = (
|
|
58
64
|
diagnosis.waste_cost_usd / diagnosis.total_cost_usd * 100
|
|
@@ -65,6 +71,22 @@ def render_diagnosis(
|
|
|
65
71
|
"~85–95% of actual billing; system framing not observable in JSONL", style="dim"
|
|
66
72
|
))
|
|
67
73
|
|
|
74
|
+
if diagnosis.subagent_costs:
|
|
75
|
+
show_depth = any(a.depth > 1 for a in diagnosis.subagent_costs)
|
|
76
|
+
tbl = Table(show_header=True, header_style="bold", box=None, padding=(0, 1))
|
|
77
|
+
tbl.add_column("Subagent", no_wrap=False, max_width=48)
|
|
78
|
+
if show_depth:
|
|
79
|
+
tbl.add_column("Depth", justify="right", width=6)
|
|
80
|
+
tbl.add_column("Cost", justify="right", width=8)
|
|
81
|
+
for a in diagnosis.subagent_costs:
|
|
82
|
+
label = a.label if len(a.label) <= 45 else a.label[:44] + "…"
|
|
83
|
+
cost_cell = f"${a.total_cost_usd:.3f}"
|
|
84
|
+
if show_depth:
|
|
85
|
+
tbl.add_row(label, str(a.depth), cost_cell)
|
|
86
|
+
else:
|
|
87
|
+
tbl.add_row(label, cost_cell)
|
|
88
|
+
con.print(tbl)
|
|
89
|
+
|
|
68
90
|
if not diagnosis.findings:
|
|
69
91
|
con.print("\nNo findings — session looks clean.")
|
|
70
92
|
return
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cctx-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.0"
|
|
8
8
|
description = "Diagnose Claude Code sessions — find what went wrong, what it cost, and what to add to CLAUDE.md"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -229,6 +229,43 @@ def test_write_produces_one_line_per_session() -> None:
|
|
|
229
229
|
assert ids == {"sess-001", "sess-002"}
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
def test_export_diagnosis_includes_subagent_costs() -> None:
|
|
233
|
+
"""JSON export includes subagent_costs array with correct fields."""
|
|
234
|
+
import dataclasses
|
|
235
|
+
import json
|
|
236
|
+
|
|
237
|
+
from cctx.exporters.jsonl import export_diagnosis
|
|
238
|
+
from cctx.models import SubagentAttribution
|
|
239
|
+
|
|
240
|
+
diag = _make_diagnosis()
|
|
241
|
+
trace = _make_trace()
|
|
242
|
+
diag = dataclasses.replace(diag, subagent_costs=[
|
|
243
|
+
SubagentAttribution(
|
|
244
|
+
session_id="child-1",
|
|
245
|
+
label="My task",
|
|
246
|
+
total_cost_usd=0.020,
|
|
247
|
+
depth=1,
|
|
248
|
+
model="claude-sonnet-4",
|
|
249
|
+
)
|
|
250
|
+
])
|
|
251
|
+
data = json.loads(export_diagnosis(diag, trace))
|
|
252
|
+
assert "subagent_costs" in data
|
|
253
|
+
assert len(data["subagent_costs"]) == 1
|
|
254
|
+
assert data["subagent_costs"][0]["session_id"] == "child-1"
|
|
255
|
+
assert data["subagent_costs"][0]["cost_usd"] == pytest.approx(0.020)
|
|
256
|
+
assert data["subagent_costs"][0]["depth"] == 1
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_export_diagnosis_subagent_costs_empty_by_default() -> None:
|
|
260
|
+
"""JSON export has subagent_costs: [] when no subagents."""
|
|
261
|
+
from cctx.exporters.jsonl import export_diagnosis
|
|
262
|
+
|
|
263
|
+
diag = _make_diagnosis()
|
|
264
|
+
trace = _make_trace()
|
|
265
|
+
data = json.loads(export_diagnosis(diag, trace))
|
|
266
|
+
assert data["subagent_costs"] == []
|
|
267
|
+
|
|
268
|
+
|
|
232
269
|
def test_write_empty_list_produces_no_output() -> None:
|
|
233
270
|
"""write() with an empty list produces no output."""
|
|
234
271
|
from cctx.exporters.jsonl import write
|
|
@@ -266,6 +266,29 @@ def test_autopsy_html_flag_writes_file(tmp_path):
|
|
|
266
266
|
assert session_id in content
|
|
267
267
|
|
|
268
268
|
|
|
269
|
+
def test_html_includes_subagent_costs():
|
|
270
|
+
"""HTML output contains subagent label and cost when subagent_costs present."""
|
|
271
|
+
import dataclasses
|
|
272
|
+
|
|
273
|
+
from cctx.models import SubagentAttribution
|
|
274
|
+
from cctx.renderers.report import render_html
|
|
275
|
+
|
|
276
|
+
diag = _make_diagnosis()
|
|
277
|
+
trace = _simple_trace()
|
|
278
|
+
diag = dataclasses.replace(diag, subagent_costs=[
|
|
279
|
+
SubagentAttribution(
|
|
280
|
+
session_id="child-1",
|
|
281
|
+
label="Analyze the database schema",
|
|
282
|
+
total_cost_usd=0.025,
|
|
283
|
+
depth=1,
|
|
284
|
+
model="claude-sonnet-4",
|
|
285
|
+
)
|
|
286
|
+
])
|
|
287
|
+
html = render_html(diag, trace)
|
|
288
|
+
assert "Analyze the database schema" in html
|
|
289
|
+
assert "0.025" in html
|
|
290
|
+
|
|
291
|
+
|
|
269
292
|
def test_autopsy_html_with_since_errors(tmp_path):
|
|
270
293
|
"""--html and --since together should error, not silently ignore --html."""
|
|
271
294
|
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Tests for per-subagent cost attribution (M16 #88)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cctx.models import Diagnosis, SessionTrace, ToolUse, Turn, Usage
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# Helpers — synthetic trace builders (real fixtures have scrubbed tokens)
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
_TS = datetime(2026, 6, 10, tzinfo=timezone.utc)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_usage(input_tokens: int) -> Usage:
|
|
17
|
+
return Usage(
|
|
18
|
+
input_tokens=input_tokens,
|
|
19
|
+
output_tokens=50,
|
|
20
|
+
cache_creation_5m=0,
|
|
21
|
+
cache_creation_1h=0,
|
|
22
|
+
cache_read=0,
|
|
23
|
+
service_tier=None,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_trace(
|
|
28
|
+
session_id: str,
|
|
29
|
+
input_tokens: int,
|
|
30
|
+
*,
|
|
31
|
+
subagents: list[SessionTrace] | None = None,
|
|
32
|
+
model: str = "claude-sonnet-4",
|
|
33
|
+
tool_uses: list[ToolUse] | None = None,
|
|
34
|
+
) -> SessionTrace:
|
|
35
|
+
turn = Turn(
|
|
36
|
+
turn_number=1,
|
|
37
|
+
uuid="u1",
|
|
38
|
+
parent_uuid=None,
|
|
39
|
+
role="assistant",
|
|
40
|
+
text="ok",
|
|
41
|
+
thinking="",
|
|
42
|
+
tool_uses=tool_uses or [],
|
|
43
|
+
tool_results=[],
|
|
44
|
+
usage=_make_usage(input_tokens),
|
|
45
|
+
model=model,
|
|
46
|
+
stop_reason="end_turn",
|
|
47
|
+
timestamp=_TS,
|
|
48
|
+
duration_ms=None,
|
|
49
|
+
)
|
|
50
|
+
return SessionTrace(
|
|
51
|
+
session_id=session_id,
|
|
52
|
+
parent_session_id=None,
|
|
53
|
+
project_path="/p",
|
|
54
|
+
cwd="/p",
|
|
55
|
+
primary_model=model,
|
|
56
|
+
claude_code_version=None,
|
|
57
|
+
turns=[turn],
|
|
58
|
+
subagents=subagents or [],
|
|
59
|
+
attachments=[],
|
|
60
|
+
raw_tool_result_files=[],
|
|
61
|
+
initial_context_tokens=0,
|
|
62
|
+
tool_names_loaded=[],
|
|
63
|
+
start_time=_TS,
|
|
64
|
+
end_time=_TS,
|
|
65
|
+
source_path=Path(f"/p/{session_id}.jsonl"),
|
|
66
|
+
subagent_meta={},
|
|
67
|
+
warnings=[],
|
|
68
|
+
subagent_parse_errors=[],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _agent_tu(
|
|
73
|
+
session_id: str,
|
|
74
|
+
*,
|
|
75
|
+
description: str = "",
|
|
76
|
+
prompt: str = "",
|
|
77
|
+
) -> ToolUse:
|
|
78
|
+
"""Construct an Agent ToolUse linked to a child session."""
|
|
79
|
+
ti: dict = {"prompt": prompt}
|
|
80
|
+
if description:
|
|
81
|
+
ti["description"] = description
|
|
82
|
+
return ToolUse(
|
|
83
|
+
tool_name="Agent",
|
|
84
|
+
tool_use_id=f"tu_{session_id}",
|
|
85
|
+
tool_input=ti,
|
|
86
|
+
subagent_session_id=session_id,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Tests
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def test_no_subagents_cost_unchanged():
|
|
95
|
+
"""With no subagents, total_cost_usd equals parent-only cost and subagent_costs is empty."""
|
|
96
|
+
from cctx.diagnostician import run
|
|
97
|
+
trace = _make_trace("parent", input_tokens=5_000)
|
|
98
|
+
diag = run(trace)
|
|
99
|
+
assert diag.subagent_costs == []
|
|
100
|
+
# parent has 5000 input tokens at sonnet-4 price ($3/Mtok) = $0.0150
|
|
101
|
+
assert abs(diag.total_cost_usd - 0.0150) < 0.001
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_subagent_attribution_dataclass_exists():
|
|
105
|
+
from cctx.models import SubagentAttribution
|
|
106
|
+
a = SubagentAttribution(
|
|
107
|
+
session_id="child-1",
|
|
108
|
+
label="my label",
|
|
109
|
+
total_cost_usd=0.05,
|
|
110
|
+
depth=1,
|
|
111
|
+
model="claude-sonnet-4",
|
|
112
|
+
)
|
|
113
|
+
assert a.session_id == "child-1"
|
|
114
|
+
assert a.label == "my label"
|
|
115
|
+
assert a.depth == 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_one_subagent_cost_inclusive():
|
|
119
|
+
"""total_cost_usd includes direct child's cost."""
|
|
120
|
+
from cctx.diagnostician import run
|
|
121
|
+
child = _make_trace("child-1", input_tokens=10_000)
|
|
122
|
+
parent = _make_trace("parent", input_tokens=5_000, subagents=[child])
|
|
123
|
+
diag = run(parent)
|
|
124
|
+
# parent: 5000 * 3/1e6 = 0.015; child: 10000 * 3/1e6 = 0.030; total = 0.045
|
|
125
|
+
assert abs(diag.total_cost_usd - 0.045) < 0.001
|
|
126
|
+
assert len(diag.subagent_costs) == 1
|
|
127
|
+
assert diag.subagent_costs[0].session_id == "child-1"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_nested_subagents_cost_inclusive():
|
|
131
|
+
"""total_cost_usd sums all levels (parent + child + grandchild)."""
|
|
132
|
+
from cctx.diagnostician import run
|
|
133
|
+
grandchild = _make_trace("grand", input_tokens=5_000)
|
|
134
|
+
child = _make_trace("child", input_tokens=10_000, subagents=[grandchild])
|
|
135
|
+
parent = _make_trace("parent", input_tokens=5_000, subagents=[child])
|
|
136
|
+
diag = run(parent)
|
|
137
|
+
# 5000 + 10000 + 5000 = 20000 tokens * 3/1e6 = 0.060
|
|
138
|
+
assert abs(diag.total_cost_usd - 0.060) < 0.001
|
|
139
|
+
assert len(diag.subagent_costs) == 2
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_attribution_depth_1():
|
|
143
|
+
"""Direct child has depth == 1."""
|
|
144
|
+
from cctx.diagnostician import run
|
|
145
|
+
child = _make_trace("child-1", input_tokens=1_000)
|
|
146
|
+
parent = _make_trace("parent", input_tokens=1_000, subagents=[child])
|
|
147
|
+
diag = run(parent)
|
|
148
|
+
assert diag.subagent_costs[0].depth == 1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_attribution_depth_2():
|
|
152
|
+
"""Grandchild has depth == 2."""
|
|
153
|
+
from cctx.diagnostician import run
|
|
154
|
+
grandchild = _make_trace("grand", input_tokens=1_000)
|
|
155
|
+
child = _make_trace("child", input_tokens=1_000, subagents=[grandchild])
|
|
156
|
+
parent = _make_trace("parent", input_tokens=1_000, subagents=[child])
|
|
157
|
+
diag = run(parent)
|
|
158
|
+
depths = {a.session_id: a.depth for a in diag.subagent_costs}
|
|
159
|
+
assert depths["child"] == 1
|
|
160
|
+
assert depths["grand"] == 2
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_attribution_label_from_description():
|
|
164
|
+
"""Label comes from Agent tool_input['description'] when present."""
|
|
165
|
+
from cctx.diagnostician import run
|
|
166
|
+
child = _make_trace("child-1", input_tokens=1_000)
|
|
167
|
+
tu = _agent_tu("child-1", description="Explore the codebase", prompt="Do something long")
|
|
168
|
+
parent = _make_trace("parent", input_tokens=1_000, subagents=[child], tool_uses=[tu])
|
|
169
|
+
diag = run(parent)
|
|
170
|
+
assert diag.subagent_costs[0].label == "Explore the codebase"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_attribution_label_from_prompt_fallback():
|
|
174
|
+
"""When no 'description', label is prompt[:80]."""
|
|
175
|
+
from cctx.diagnostician import run
|
|
176
|
+
child = _make_trace("child-1", input_tokens=1_000)
|
|
177
|
+
long_prompt = "A" * 200
|
|
178
|
+
tu = _agent_tu("child-1", prompt=long_prompt)
|
|
179
|
+
parent = _make_trace("parent", input_tokens=1_000, subagents=[child], tool_uses=[tu])
|
|
180
|
+
diag = run(parent)
|
|
181
|
+
assert diag.subagent_costs[0].label == long_prompt[:80]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_attribution_label_orphan_fallback():
|
|
185
|
+
"""Unlinked subagent (no matching ToolUse) gets session_id[:12] as label."""
|
|
186
|
+
from cctx.diagnostician import run
|
|
187
|
+
child = _make_trace("child-unlinked-session", input_tokens=1_000)
|
|
188
|
+
# Parent has no Agent ToolUse linking to this child
|
|
189
|
+
parent = _make_trace("parent", input_tokens=1_000, subagents=[child])
|
|
190
|
+
diag = run(parent)
|
|
191
|
+
assert diag.subagent_costs[0].label == "child-unlink" # first 12 chars
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_subagent_cost_no_double_count():
|
|
195
|
+
"""Two direct subagents: total equals parent + child1 + child2."""
|
|
196
|
+
from cctx.diagnostician import run
|
|
197
|
+
child1 = _make_trace("c1", input_tokens=10_000)
|
|
198
|
+
child2 = _make_trace("c2", input_tokens=20_000)
|
|
199
|
+
parent = _make_trace("parent", input_tokens=5_000, subagents=[child1, child2])
|
|
200
|
+
diag = run(parent)
|
|
201
|
+
expected = (5_000 + 10_000 + 20_000) * 3 / 1_000_000
|
|
202
|
+
assert abs(diag.total_cost_usd - expected) < 0.001
|
|
203
|
+
assert len(diag.subagent_costs) == 2
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_total_cost_not_less_than_depth1_sum():
|
|
207
|
+
"""Invariant: total_cost >= sum of direct-child costs."""
|
|
208
|
+
from cctx.diagnostician import run
|
|
209
|
+
child1 = _make_trace("c1", input_tokens=10_000)
|
|
210
|
+
child2 = _make_trace("c2", input_tokens=20_000)
|
|
211
|
+
parent = _make_trace("parent", input_tokens=5_000, subagents=[child1, child2])
|
|
212
|
+
diag = run(parent)
|
|
213
|
+
depth1_sum = sum(a.total_cost_usd for a in diag.subagent_costs if a.depth == 1)
|
|
214
|
+
assert diag.total_cost_usd >= depth1_sum
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# Renderer tests
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def _make_diagnosis_with_subagents(n: int = 2) -> Diagnosis:
|
|
222
|
+
from cctx.models import Diagnosis, SubagentAttribution
|
|
223
|
+
attributions = [
|
|
224
|
+
SubagentAttribution(
|
|
225
|
+
session_id=f"child-{i}",
|
|
226
|
+
label=f"Task {i}: do something useful",
|
|
227
|
+
total_cost_usd=round(0.010 * (i + 1), 4),
|
|
228
|
+
depth=1,
|
|
229
|
+
model="claude-sonnet-4",
|
|
230
|
+
)
|
|
231
|
+
for i in range(n)
|
|
232
|
+
]
|
|
233
|
+
return Diagnosis(
|
|
234
|
+
session_id="parent-session",
|
|
235
|
+
findings=[],
|
|
236
|
+
inflection_turn=None,
|
|
237
|
+
patches=[],
|
|
238
|
+
total_cost_usd=round(0.030 + sum(a.total_cost_usd for a in attributions), 4),
|
|
239
|
+
waste_cost_usd=0.0,
|
|
240
|
+
analysed_at=_TS,
|
|
241
|
+
subagent_costs=attributions,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_render_diagnosis_shows_subagent_summary():
|
|
246
|
+
"""Cost line mentions subagent count and sum when subagents present."""
|
|
247
|
+
from io import StringIO
|
|
248
|
+
|
|
249
|
+
from rich.console import Console
|
|
250
|
+
|
|
251
|
+
from cctx.renderers.terminal import render_diagnosis
|
|
252
|
+
buf = StringIO()
|
|
253
|
+
con = Console(file=buf, no_color=True, width=120)
|
|
254
|
+
diag = _make_diagnosis_with_subagents(2)
|
|
255
|
+
render_diagnosis(diag, console=con)
|
|
256
|
+
out = buf.getvalue()
|
|
257
|
+
assert "2 subagent" in out
|
|
258
|
+
assert "$0.03" in out # subagent sum = 0.010 + 0.020 = 0.030
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_render_diagnosis_shows_subagent_table():
|
|
262
|
+
"""Subagent table lists each agent's label and cost."""
|
|
263
|
+
from io import StringIO
|
|
264
|
+
|
|
265
|
+
from rich.console import Console
|
|
266
|
+
|
|
267
|
+
from cctx.renderers.terminal import render_diagnosis
|
|
268
|
+
buf = StringIO()
|
|
269
|
+
con = Console(file=buf, no_color=True, width=120)
|
|
270
|
+
diag = _make_diagnosis_with_subagents(2)
|
|
271
|
+
render_diagnosis(diag, console=con)
|
|
272
|
+
out = buf.getvalue()
|
|
273
|
+
assert "Task 0: do something useful" in out
|
|
274
|
+
assert "Task 1: do something useful" in out
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_render_diagnosis_no_subagents_no_table():
|
|
278
|
+
"""When subagent_costs is empty, no subagent table is shown."""
|
|
279
|
+
from io import StringIO
|
|
280
|
+
|
|
281
|
+
from rich.console import Console
|
|
282
|
+
|
|
283
|
+
from cctx.models import Diagnosis
|
|
284
|
+
from cctx.renderers.terminal import render_diagnosis
|
|
285
|
+
buf = StringIO()
|
|
286
|
+
con = Console(file=buf, no_color=True, width=120)
|
|
287
|
+
diag = Diagnosis(
|
|
288
|
+
session_id="s1",
|
|
289
|
+
findings=[],
|
|
290
|
+
inflection_turn=None,
|
|
291
|
+
patches=[],
|
|
292
|
+
total_cost_usd=0.05,
|
|
293
|
+
waste_cost_usd=0.0,
|
|
294
|
+
analysed_at=_TS,
|
|
295
|
+
)
|
|
296
|
+
render_diagnosis(diag, console=con)
|
|
297
|
+
out = buf.getvalue()
|
|
298
|
+
assert "subagent" not in out.lower()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|