cctx-cli 1.5.1__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.5.1 → cctx_cli-1.7.0}/CHANGELOG.md +100 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/PKG-INFO +1 -1
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/PRODUCT.md +1 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/cli.py +36 -1
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/__init__.py +54 -8
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/jsonl.py +10 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/harvest.py +74 -4
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/models.py +28 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/templates/autopsy.html.j2 +15 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/terminal.py +22 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +22 -6
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/pyproject.toml +1 -1
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/exporters/test_jsonl.py +37 -0
- {cctx_cli-1.5.1 → 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.7.0/tests/test_harvest_emit.py +204 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.gitignore +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/CLAUDE.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/DESIGN.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/README.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/action.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/agents.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/recommender/claude_md.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/demo.gif +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/demo.tape +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/conftest.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_models.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,106 @@
|
|
|
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
|
+
|
|
46
|
+
## v1.6.0 (2026-06-10)
|
|
47
|
+
|
|
48
|
+
### Bug Fixes
|
|
49
|
+
|
|
50
|
+
- Harvest — preview_patches dedup per (target, heading) not heading-only
|
|
51
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
52
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
53
|
+
|
|
54
|
+
- Harvest — shorten local-import comment under 100-char line limit
|
|
55
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
56
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
57
|
+
|
|
58
|
+
### Documentation
|
|
59
|
+
|
|
60
|
+
- Harvest — correct misleading local-import comment
|
|
61
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
62
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
63
|
+
|
|
64
|
+
- Spec deviation note (sync returns patches) + PRODUCT.md cross-agent emit row
|
|
65
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
66
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
67
|
+
|
|
68
|
+
### Features
|
|
69
|
+
|
|
70
|
+
- Cctx harvest --emit — cross-agent layer to AGENTS.md (#82)
|
|
71
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
72
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
73
|
+
|
|
74
|
+
- Cli — harvest --emit / --sync cross-agent emit
|
|
75
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
76
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
77
|
+
|
|
78
|
+
- Harvest — EMIT_TARGETS + retarget_patches (fan-out to AGENTS.md)
|
|
79
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
80
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
81
|
+
|
|
82
|
+
- Harvest — sync_managed_sections backfills CLAUDE.md into emit target
|
|
83
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
84
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
85
|
+
|
|
86
|
+
- Models — MANAGED_HEADINGS registry for cctx-owned CLAUDE.md sections
|
|
87
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
88
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
89
|
+
|
|
90
|
+
### Testing
|
|
91
|
+
|
|
92
|
+
- Emit + sync idempotency through apply_patches
|
|
93
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
94
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
95
|
+
|
|
96
|
+
- End-to-end fan-out to both targets; spec: reconcile sync error contract
|
|
97
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
98
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
99
|
+
|
|
100
|
+
- Lock MANAGED_HEADINGS registry to recommender templates
|
|
101
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
102
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
103
|
+
|
|
104
|
+
|
|
5
105
|
## v1.5.1 (2026-06-10)
|
|
6
106
|
|
|
7
107
|
### Bug Fixes
|
|
@@ -76,6 +76,7 @@ Six commands (`ls`, `autopsy`, `harvest`, `watch`, `trace`, `export`). No comman
|
|
|
76
76
|
| Memory-hygiene depth | `harvest --check` + `--check-severity` | v1.4.0 (M13) |
|
|
77
77
|
| Live session badges | `cctx ls` | unreleased |
|
|
78
78
|
| Live session detection, early idle exit | `cctx watch` | unreleased |
|
|
79
|
+
| Cross-agent emit | `cctx harvest --emit agents [--sync]` | M15; mirror CLAUDE.md sections to AGENTS.md — unreleased |
|
|
79
80
|
|
|
80
81
|
### Pattern classifiers (v1.4.0)
|
|
81
82
|
|
|
@@ -23,6 +23,7 @@ from cctx.agents import live_sessions as _live_sessions
|
|
|
23
23
|
from cctx.diagnostician import aggregate
|
|
24
24
|
from cctx.diagnostician.patterns import project_specific
|
|
25
25
|
from cctx.discovery import complete_project as _complete_project
|
|
26
|
+
from cctx.harvest import EMIT_TARGETS
|
|
26
27
|
from cctx.models import KIND_LABEL, AggregateReport
|
|
27
28
|
from cctx.parsers.claude_code import parse_session
|
|
28
29
|
from cctx.recommender import claude_md
|
|
@@ -570,6 +571,22 @@ def trace(target: Path | None, latest: bool) -> None:
|
|
|
570
571
|
show_default=True,
|
|
571
572
|
help="Minimum severity that causes --check to exit 1.",
|
|
572
573
|
)
|
|
574
|
+
@click.option(
|
|
575
|
+
"--emit",
|
|
576
|
+
"emit_targets",
|
|
577
|
+
multiple=True,
|
|
578
|
+
type=click.Choice(list(EMIT_TARGETS)),
|
|
579
|
+
help="Also write applicable patches to another agent's instruction file "
|
|
580
|
+
"(e.g. AGENTS.md). Repeatable.",
|
|
581
|
+
)
|
|
582
|
+
@click.option(
|
|
583
|
+
"--sync",
|
|
584
|
+
"sync_mode",
|
|
585
|
+
is_flag=True,
|
|
586
|
+
default=False,
|
|
587
|
+
help="With --emit: also mirror already-harvested cctx-managed sections "
|
|
588
|
+
"from CLAUDE.md into the emit target.",
|
|
589
|
+
)
|
|
573
590
|
def harvest(
|
|
574
591
|
target: Path,
|
|
575
592
|
since: str | None,
|
|
@@ -578,9 +595,20 @@ def harvest(
|
|
|
578
595
|
target_dir: Path | None,
|
|
579
596
|
check_mode: bool,
|
|
580
597
|
check_severity: str,
|
|
598
|
+
emit_targets: tuple[str, ...],
|
|
599
|
+
sync_mode: bool,
|
|
581
600
|
) -> None:
|
|
582
601
|
"""Apply autopsy patches to CLAUDE.md."""
|
|
583
|
-
from cctx.harvest import
|
|
602
|
+
from cctx.harvest import (
|
|
603
|
+
apply_patches,
|
|
604
|
+
check_claude_md,
|
|
605
|
+
preview_patches,
|
|
606
|
+
retarget_patches,
|
|
607
|
+
sync_managed_sections,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if sync_mode and not emit_targets:
|
|
611
|
+
raise click.UsageError("--sync requires --emit.")
|
|
584
612
|
|
|
585
613
|
if check_mode:
|
|
586
614
|
from cctx.harvest import CheckSeverity
|
|
@@ -626,6 +654,13 @@ def harvest(
|
|
|
626
654
|
diagnosis = claude_md.generate(diagnosis)
|
|
627
655
|
patches = diagnosis.patches
|
|
628
656
|
|
|
657
|
+
base = patches
|
|
658
|
+
for t in emit_targets:
|
|
659
|
+
emitted = retarget_patches(base, t)
|
|
660
|
+
if sync_mode:
|
|
661
|
+
emitted = emitted + sync_managed_sections(resolved_dir, t)
|
|
662
|
+
patches = patches + emitted
|
|
663
|
+
|
|
629
664
|
if not patches:
|
|
630
665
|
render_harvest_results([], dry_run=dry_run)
|
|
631
666
|
return
|
|
@@ -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
|
|
|
@@ -13,6 +13,7 @@ Layering rules (MUST respect):
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import dataclasses
|
|
16
17
|
import re
|
|
17
18
|
from collections import defaultdict
|
|
18
19
|
from dataclasses import dataclass
|
|
@@ -20,6 +21,8 @@ from enum import Enum
|
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import TYPE_CHECKING
|
|
22
23
|
|
|
24
|
+
from cctx.models import MANAGED_HEADING_PREFIX, MANAGED_HEADINGS
|
|
25
|
+
|
|
23
26
|
if TYPE_CHECKING:
|
|
24
27
|
from cctx.models import Patch
|
|
25
28
|
|
|
@@ -105,6 +108,70 @@ def _is_supported_target(patch: Patch) -> bool:
|
|
|
105
108
|
# Public API
|
|
106
109
|
# ---------------------------------------------------------------------------
|
|
107
110
|
|
|
111
|
+
# Maps an --emit target name to the destination filename. Single place to add
|
|
112
|
+
# future targets (Cursor, Windsurf, Copilot) when demand exists.
|
|
113
|
+
EMIT_TARGETS: dict[str, str] = {
|
|
114
|
+
"agents": "AGENTS.md",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def retarget_patches(patches: list[Patch], emit_target: str) -> list[Patch]:
|
|
119
|
+
"""Clone CLAUDE.md-targeted patches to the emit target's file.
|
|
120
|
+
|
|
121
|
+
Only patches whose target_file is exactly "CLAUDE.md" are emitted —
|
|
122
|
+
.claude/rules/ and .claude/skills/ patches are Claude Code-specific and do
|
|
123
|
+
not translate to other agents. Returns clones; inputs are unmodified.
|
|
124
|
+
"""
|
|
125
|
+
dest = EMIT_TARGETS[emit_target]
|
|
126
|
+
return [
|
|
127
|
+
dataclasses.replace(p, target_file=dest)
|
|
128
|
+
for p in patches
|
|
129
|
+
if p.target_file == "CLAUDE.md"
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Reverse map: exact managed heading -> the FindingKind that owns it.
|
|
134
|
+
_HEADING_TO_KIND = {heading: kind for kind, heading in MANAGED_HEADINGS.items()}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def sync_managed_sections(target_dir: Path, emit_target: str) -> list[Patch]:
|
|
138
|
+
"""Build synthetic patches mirroring CLAUDE.md's cctx-managed sections.
|
|
139
|
+
|
|
140
|
+
Reads CLAUDE.md in target_dir, keeps sections whose heading is an exact
|
|
141
|
+
MANAGED_HEADINGS value or starts with MANAGED_HEADING_PREFIX, and returns
|
|
142
|
+
one Patch per kept section targeting the emit file. Returns [] if CLAUDE.md
|
|
143
|
+
is absent. The CLI routes these through preview_patches / apply_patches, so
|
|
144
|
+
idempotency and dry-run come for free from the existing machinery.
|
|
145
|
+
"""
|
|
146
|
+
from cctx.models import FindingKind, Patch # runtime use (Patch is TYPE_CHECKING-only above)
|
|
147
|
+
|
|
148
|
+
claude_md = target_dir / "CLAUDE.md"
|
|
149
|
+
if not claude_md.exists():
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
dest = EMIT_TARGETS[emit_target]
|
|
153
|
+
content = claude_md.read_text(encoding="utf-8")
|
|
154
|
+
patches: list[Patch] = []
|
|
155
|
+
|
|
156
|
+
for heading, body in _parse_sections(content):
|
|
157
|
+
is_fixed = heading in _HEADING_TO_KIND
|
|
158
|
+
is_prefixed = heading.startswith(MANAGED_HEADING_PREFIX)
|
|
159
|
+
if not (is_fixed or is_prefixed):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
kind = _HEADING_TO_KIND[heading] if is_fixed else FindingKind.PROJECT_PATTERN
|
|
163
|
+
diff_lines = [heading] + body.splitlines()
|
|
164
|
+
unified_diff = "\n".join(f"+{line}" for line in diff_lines)
|
|
165
|
+
patches.append(Patch(
|
|
166
|
+
target_file=dest,
|
|
167
|
+
description=heading,
|
|
168
|
+
unified_diff=unified_diff,
|
|
169
|
+
finding_kind=kind,
|
|
170
|
+
evidence_summary="synced from CLAUDE.md",
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
return patches
|
|
174
|
+
|
|
108
175
|
|
|
109
176
|
def apply_patch(patch: Patch, target_dir: Path) -> ApplyResult:
|
|
110
177
|
"""Apply one patch. Never raises — errors go into ApplyResult(status=ERROR)."""
|
|
@@ -161,8 +228,10 @@ def apply_patch(patch: Patch, target_dir: Path) -> ApplyResult:
|
|
|
161
228
|
def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]:
|
|
162
229
|
"""Compute what would happen without writing. Returns APPLIED or SKIPPED."""
|
|
163
230
|
results = []
|
|
164
|
-
# Track
|
|
165
|
-
|
|
231
|
+
# Track (target_path, fingerprint) pairs already "seen" within this preview
|
|
232
|
+
# run (idempotency). Keyed by file so the same heading in two different
|
|
233
|
+
# target files is correctly treated as two independent patches.
|
|
234
|
+
seen_fingerprints: set[tuple[Path, str]] = set()
|
|
166
235
|
|
|
167
236
|
for patch in patches:
|
|
168
237
|
target_path = target_dir / patch.target_file
|
|
@@ -181,7 +250,8 @@ def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]
|
|
|
181
250
|
|
|
182
251
|
content = target_path.read_text(encoding="utf-8") if target_path.exists() else ""
|
|
183
252
|
|
|
184
|
-
|
|
253
|
+
already_seen = fp is not None and (target_path, fp) in seen_fingerprints
|
|
254
|
+
if fp is not None and (_already_present(content, fp) or already_seen):
|
|
185
255
|
results.append(ApplyResult(
|
|
186
256
|
patch=patch,
|
|
187
257
|
status=ApplyStatus.SKIPPED,
|
|
@@ -190,7 +260,7 @@ def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]
|
|
|
190
260
|
))
|
|
191
261
|
else:
|
|
192
262
|
if fp is not None:
|
|
193
|
-
seen_fingerprints.add(fp)
|
|
263
|
+
seen_fingerprints.add((target_path, fp))
|
|
194
264
|
results.append(ApplyResult(
|
|
195
265
|
patch=patch,
|
|
196
266
|
status=ApplyStatus.APPLIED,
|
|
@@ -184,6 +184,22 @@ KIND_LABEL: dict[FindingKind, str] = {
|
|
|
184
184
|
FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
# Maps FindingKind to the exact ## heading emitted by its recommender patch
|
|
188
|
+
# template. Single source of truth for "which CLAUDE.md sections cctx owns."
|
|
189
|
+
# harvest.py imports this (never reaches into recommender/) so emit/sync can
|
|
190
|
+
# identify cctx-managed sections without depending on the patch generator.
|
|
191
|
+
MANAGED_HEADINGS: dict[FindingKind, str] = {
|
|
192
|
+
FindingKind.RETRY_LOOP: "## Retry discipline",
|
|
193
|
+
FindingKind.SCOPE_CREEP: "## Scope discipline",
|
|
194
|
+
FindingKind.STALE_CONTEXT: "## Context hygiene",
|
|
195
|
+
FindingKind.TOOL_THRASH: "## Tool-call discipline",
|
|
196
|
+
FindingKind.DEAD_END: "## Exploration discipline",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Project-specific patterns use a heading that embeds tool+key, so the managed
|
|
200
|
+
# section is identified by prefix rather than exact match.
|
|
201
|
+
MANAGED_HEADING_PREFIX: str = "## Project-specific: "
|
|
202
|
+
|
|
187
203
|
|
|
188
204
|
class Severity(str, Enum):
|
|
189
205
|
HIGH = "high"
|
|
@@ -217,6 +233,17 @@ class Patch:
|
|
|
217
233
|
evidence_summary: str
|
|
218
234
|
|
|
219
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
|
+
|
|
220
247
|
@dataclass
|
|
221
248
|
class Diagnosis:
|
|
222
249
|
session_id: str
|
|
@@ -226,6 +253,7 @@ class Diagnosis:
|
|
|
226
253
|
total_cost_usd: float
|
|
227
254
|
waste_cost_usd: float
|
|
228
255
|
analysed_at: datetime
|
|
256
|
+
subagent_costs: list[SubagentAttribution] = field(default_factory=list)
|
|
229
257
|
|
|
230
258
|
@property
|
|
231
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
|
{cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md
RENAMED
|
@@ -88,12 +88,21 @@ Clones patches suitable for emission:
|
|
|
88
88
|
reach here given the above filter, but stated explicitly for clarity).
|
|
89
89
|
- Returns clones via `dataclasses.replace(patch, target_file=EMIT_TARGETS[emit_target])`.
|
|
90
90
|
|
|
91
|
-
### `sync_managed_sections(target_dir, emit_target) -> list[
|
|
91
|
+
### `sync_managed_sections(target_dir, emit_target) -> list[Patch]`
|
|
92
|
+
|
|
93
|
+
> **Implementation deviation (2026-06-10):** This function returns
|
|
94
|
+
> `list[Patch]` rather than applying patches inline (the original draft returned
|
|
95
|
+
> `list[ApplyResult]` and called `apply_patch` itself). The CLI appends these
|
|
96
|
+
> patches to the same list it routes through `preview_patches` / `apply_patches`.
|
|
97
|
+
> Returning patches keeps `--dry-run` write-free by construction and matches the
|
|
98
|
+
> codebase's "CLI decides preview vs. apply" layering — applying inline could not
|
|
99
|
+
> preview, contradicting the `--dry-run` requirement and `test_dry_run_no_writes`.
|
|
92
100
|
|
|
93
101
|
1. Reads `CLAUDE.md` from `target_dir`. Returns empty list if absent.
|
|
94
102
|
2. Calls `_parse_sections(content)` (already in `harvest.py`).
|
|
95
103
|
3. Keeps sections whose heading is exactly in `MANAGED_HEADINGS.values()` OR
|
|
96
|
-
starts with `MANAGED_HEADING_PREFIX`.
|
|
104
|
+
starts with `MANAGED_HEADING_PREFIX`. The leading `("(preamble)", …)` pair
|
|
105
|
+
matches neither branch and is skipped.
|
|
97
106
|
4. For each kept section, constructs a synthetic `Patch` with:
|
|
98
107
|
- `target_file = EMIT_TARGETS[emit_target]`
|
|
99
108
|
- `unified_diff = "\n".join(f"+{line}" for line in [heading] + body.splitlines())`
|
|
@@ -102,8 +111,10 @@ Clones patches suitable for emission:
|
|
|
102
111
|
`FindingKind.PROJECT_PATTERN` for `## Project-specific: …` prefixed headings
|
|
103
112
|
- `description = heading`
|
|
104
113
|
- `evidence_summary = "synced from CLAUDE.md"`
|
|
105
|
-
5.
|
|
106
|
-
|
|
114
|
+
5. Returns the list of synthetic patches. The CLI routes them through the
|
|
115
|
+
existing `preview_patches` / `apply_patches` machinery, which handles
|
|
116
|
+
idempotency via `_already_present` (the `## Heading` line is the fingerprint)
|
|
117
|
+
and dry-run preview without writing.
|
|
107
118
|
|
|
108
119
|
---
|
|
109
120
|
|
|
@@ -143,8 +154,13 @@ under their full path.
|
|
|
143
154
|
## Error contract
|
|
144
155
|
|
|
145
156
|
- Never raises. All failures return `ApplyResult(status=ERROR, message=...)`.
|
|
146
|
-
- `--sync` with no CLAUDE.md: returns `[]` (not an
|
|
147
|
-
|
|
157
|
+
- `--sync` with no CLAUDE.md: `sync_managed_sections` returns `[]` (not an
|
|
158
|
+
error). Because it returns patches rather than applying inline (see the
|
|
159
|
+
deviation note above), there is no `SKIPPED` line for the missing file — the
|
|
160
|
+
empty result simply contributes nothing, and if no other patches exist the CLI
|
|
161
|
+
prints its standard "No patches to apply." message. (The original draft
|
|
162
|
+
emitted a "CLAUDE.md not found — nothing to sync." line; that belonged to the
|
|
163
|
+
inline-apply design and no longer applies.)
|
|
148
164
|
- Emit target directory is created by `apply_patch`'s existing `parent.mkdir`.
|
|
149
165
|
|
|
150
166
|
---
|
|
@@ -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
|