cctx-cli 1.13.0__tar.gz → 1.15.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.13.0 → cctx_cli-1.15.0}/CHANGELOG.md +18 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/PKG-INFO +1 -1
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/cli.py +9 -1
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/__init__.py +2 -0
- cctx_cli-1.15.0/cctx/diagnostician/patterns/compaction.py +114 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/dead_end.py +2 -1
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/stale_context.py +4 -2
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/models.py +3 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/recommender/claude_md.py +10 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/renderers/terminal.py +35 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/pyproject.toml +1 -1
- cctx_cli-1.15.0/tests/test_compaction_classifier.py +373 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_harvest_emit.py +1 -0
- cctx_cli-1.15.0/tests/test_savings_framing.py +202 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/.gitignore +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/CLAUDE.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/DESIGN.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/PRODUCT.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/README.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/action.yml +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/agents.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/cache_hygiene.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/fan_out.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/harvest.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/hook_installer.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/parsers/otel.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/demo.gif +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/demo.tape +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/quickstart-otel.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/plans/2026-06-19-otel-parser.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/docs/superpowers/specs/2026-06-19-otel-parser-design.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/conftest.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/otel_fanout.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/otel_handoff.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_cache_hygiene_classifier.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_diagnostician_subagents.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_efficacy.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_fanout_classifier.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_init.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_models.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_otel_parser.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.13.0 → cctx_cli-1.15.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.15.0 (2026-06-20)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Compaction findings — surface compaction events and re-fetch waste (#93)
|
|
10
|
+
([#123](https://github.com/jacquardlabs/cctx/pull/123),
|
|
11
|
+
[`cdab064`](https://github.com/jacquardlabs/cctx/commit/cdab0640b720e127549af63c22747555bd832b2c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## v1.14.0 (2026-06-20)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- Savings framing + health grade behind --health flag (#101)
|
|
19
|
+
([#122](https://github.com/jacquardlabs/cctx/pull/122),
|
|
20
|
+
[`ee4bc28`](https://github.com/jacquardlabs/cctx/commit/ee4bc2833d7260275e95666ba45a5952aa174aee))
|
|
21
|
+
|
|
22
|
+
|
|
5
23
|
## v1.13.0 (2026-06-20)
|
|
6
24
|
|
|
7
25
|
### Documentation
|
|
@@ -346,6 +346,13 @@ def ls(project: Path | None) -> None:
|
|
|
346
346
|
help="Print one verdict line only when findings exist; nothing if clean. "
|
|
347
347
|
"Designed for SessionEnd hook use (cctx init).",
|
|
348
348
|
)
|
|
349
|
+
@click.option(
|
|
350
|
+
"--health",
|
|
351
|
+
"health",
|
|
352
|
+
is_flag=True,
|
|
353
|
+
default=False,
|
|
354
|
+
help="Show health grade (A–F) and per-finding savings estimate.",
|
|
355
|
+
)
|
|
349
356
|
def autopsy(
|
|
350
357
|
target: Path | None,
|
|
351
358
|
since: str | None,
|
|
@@ -358,6 +365,7 @@ def autopsy(
|
|
|
358
365
|
turn_num: int | None,
|
|
359
366
|
json_out: bool,
|
|
360
367
|
quiet: bool,
|
|
368
|
+
health: bool,
|
|
361
369
|
) -> None:
|
|
362
370
|
"""Diagnose a session or project directory.
|
|
363
371
|
|
|
@@ -488,7 +496,7 @@ def autopsy(
|
|
|
488
496
|
from cctx.renderers.github import write_github_summary
|
|
489
497
|
write_github_summary(diagnosis)
|
|
490
498
|
else:
|
|
491
|
-
render_diagnosis(diagnosis, session_path=target)
|
|
499
|
+
render_diagnosis(diagnosis, session_path=target, show_health=health)
|
|
492
500
|
if fail_on_findings and diagnosis.findings:
|
|
493
501
|
raise SystemExit(1)
|
|
494
502
|
|
|
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING
|
|
|
16
16
|
from cctx.diagnostician import inflection
|
|
17
17
|
from cctx.diagnostician.patterns import (
|
|
18
18
|
cache_hygiene,
|
|
19
|
+
compaction,
|
|
19
20
|
dead_end,
|
|
20
21
|
fan_out,
|
|
21
22
|
retry_loop,
|
|
@@ -154,6 +155,7 @@ def run(trace: SessionTrace) -> Diagnosis:
|
|
|
154
155
|
*dead_end.classify(trace),
|
|
155
156
|
*fan_out.classify(trace),
|
|
156
157
|
*cache_hygiene.classify(trace),
|
|
158
|
+
*compaction.classify(trace),
|
|
157
159
|
]
|
|
158
160
|
findings.sort(key=lambda f: f.first_turn)
|
|
159
161
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Compaction-event classifier.
|
|
2
|
+
|
|
3
|
+
Detects context-window compaction events and surfaces them as first-class
|
|
4
|
+
findings. Also attributes re-fetch waste: files read before compaction that
|
|
5
|
+
are read again after (token cost of the re-read attributed to the compaction).
|
|
6
|
+
|
|
7
|
+
Exported helpers:
|
|
8
|
+
is_compaction_turn(turn) — canonical compaction predicate used by
|
|
9
|
+
stale_context.py and dead_end.py (replaces their local implementations).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from cctx.models import Confidence, Finding, FindingKind, Severity
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from cctx.models import SessionTrace, Turn
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_compaction_turn(turn: Turn) -> bool:
|
|
22
|
+
"""True if this turn represents a context-window compaction event."""
|
|
23
|
+
if turn.role == "system" and "compact" in turn.text.lower():
|
|
24
|
+
return True
|
|
25
|
+
return turn.text.startswith("<context_window")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _classify_impl(trace: SessionTrace) -> list[Finding]:
|
|
29
|
+
compaction_turns = [t for t in trace.turns if is_compaction_turn(t)]
|
|
30
|
+
if not compaction_turns:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
first_compaction_turn = compaction_turns[0].turn_number
|
|
34
|
+
|
|
35
|
+
# Build map of files read before first compaction: key → token_count
|
|
36
|
+
pre_reads: dict[str, int] = {}
|
|
37
|
+
for turn in trace.turns:
|
|
38
|
+
if turn.turn_number >= first_compaction_turn:
|
|
39
|
+
break
|
|
40
|
+
for tu in turn.tool_uses:
|
|
41
|
+
if tu.tool_name == "Read":
|
|
42
|
+
fp = tu.tool_input.get("file_path", "")
|
|
43
|
+
if not fp:
|
|
44
|
+
continue
|
|
45
|
+
# Find matching tool result to get token count
|
|
46
|
+
for tr in turn.tool_results:
|
|
47
|
+
if tr.tool_use_id == tu.tool_use_id:
|
|
48
|
+
toks = (
|
|
49
|
+
tr.token_count
|
|
50
|
+
if tr.token_count > 0
|
|
51
|
+
else len(tr.content.split()) * 4 // 3
|
|
52
|
+
)
|
|
53
|
+
pre_reads[f"Read:{fp}"] = toks
|
|
54
|
+
|
|
55
|
+
# Detect re-fetches after compaction (first occurrence only per file)
|
|
56
|
+
re_fetches: list[dict] = []
|
|
57
|
+
for turn in trace.turns:
|
|
58
|
+
if turn.turn_number <= first_compaction_turn:
|
|
59
|
+
continue
|
|
60
|
+
for tu in turn.tool_uses:
|
|
61
|
+
if tu.tool_name == "Read":
|
|
62
|
+
fp = tu.tool_input.get("file_path", "")
|
|
63
|
+
key = f"Read:{fp}"
|
|
64
|
+
if key in pre_reads:
|
|
65
|
+
re_fetches.append({
|
|
66
|
+
"tool_name": tu.tool_name,
|
|
67
|
+
"path": fp,
|
|
68
|
+
"turn": turn.turn_number,
|
|
69
|
+
"tokens": pre_reads[key],
|
|
70
|
+
})
|
|
71
|
+
del pre_reads[key] # only flag first re-fetch per file
|
|
72
|
+
|
|
73
|
+
total_refetch_tokens = sum(r["tokens"] for r in re_fetches)
|
|
74
|
+
n_compactions = len(compaction_turns)
|
|
75
|
+
compaction_turn_numbers = [t.turn_number for t in compaction_turns]
|
|
76
|
+
|
|
77
|
+
severity = Severity.HIGH if re_fetches else Severity.LOW
|
|
78
|
+
confidence = Confidence.HIGH
|
|
79
|
+
|
|
80
|
+
parts = [
|
|
81
|
+
f"{n_compactions} compaction event{'s' if n_compactions > 1 else ''} "
|
|
82
|
+
f"(turn{'s' if n_compactions > 1 else ''} "
|
|
83
|
+
f"{', '.join(str(n) for n in compaction_turn_numbers)})"
|
|
84
|
+
]
|
|
85
|
+
if re_fetches:
|
|
86
|
+
n_files = len(re_fetches)
|
|
87
|
+
parts.append(
|
|
88
|
+
f"{n_files} file{'s' if n_files > 1 else ''} re-fetched after compaction "
|
|
89
|
+
f"(~{total_refetch_tokens:,} tokens)"
|
|
90
|
+
)
|
|
91
|
+
summary = "; ".join(parts)
|
|
92
|
+
|
|
93
|
+
return [Finding(
|
|
94
|
+
kind=FindingKind.COMPACTION,
|
|
95
|
+
severity=severity,
|
|
96
|
+
confidence=confidence,
|
|
97
|
+
first_turn=compaction_turn_numbers[0],
|
|
98
|
+
last_turn=compaction_turn_numbers[-1],
|
|
99
|
+
evidence={
|
|
100
|
+
"n_compactions": n_compactions,
|
|
101
|
+
"compaction_turns": compaction_turn_numbers,
|
|
102
|
+
"re_fetches": re_fetches,
|
|
103
|
+
"total_refetch_tokens": total_refetch_tokens,
|
|
104
|
+
},
|
|
105
|
+
cost_usd=None,
|
|
106
|
+
summary=summary,
|
|
107
|
+
)]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def classify(trace: SessionTrace) -> list[Finding]:
|
|
111
|
+
try:
|
|
112
|
+
return _classify_impl(trace)
|
|
113
|
+
except Exception:
|
|
114
|
+
return []
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import json
|
|
21
21
|
from typing import TYPE_CHECKING
|
|
22
22
|
|
|
23
|
+
from cctx.diagnostician.patterns.compaction import is_compaction_turn
|
|
23
24
|
from cctx.models import Confidence, Finding, FindingKind, Severity
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -65,7 +66,7 @@ def _classify_impl(trace: SessionTrace) -> list[Finding]:
|
|
|
65
66
|
|
|
66
67
|
for turn in trace.turns:
|
|
67
68
|
# Compaction resets state
|
|
68
|
-
if turn
|
|
69
|
+
if is_compaction_turn(turn):
|
|
69
70
|
run_tool = run_key = None
|
|
70
71
|
run_count = 0
|
|
71
72
|
continue
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
|
|
15
|
+
from cctx.diagnostician.patterns.compaction import is_compaction_turn
|
|
15
16
|
from cctx.models import Confidence, Finding, FindingKind, Severity
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
@@ -33,8 +34,9 @@ def _make_3grams(text: str) -> set[tuple[str, ...]]:
|
|
|
33
34
|
return {tuple(words[i : i + 3]) for i in range(len(words) - 2)}
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
# Backwards-compat alias — external callers that import _is_compaction keep working.
|
|
36
38
|
def _is_compaction(turn: Turn) -> bool:
|
|
37
|
-
return turn
|
|
39
|
+
return is_compaction_turn(turn)
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
def _classify_impl(trace: SessionTrace) -> list[Finding]:
|
|
@@ -60,7 +62,7 @@ def _classify_impl(trace: SessionTrace) -> list[Finding]:
|
|
|
60
62
|
|
|
61
63
|
# Find the turn number of any compaction events
|
|
62
64
|
compaction_turns: set[int] = {
|
|
63
|
-
t.turn_number for t in trace.turns if
|
|
65
|
+
t.turn_number for t in trace.turns if is_compaction_turn(t)
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
last_turn_number = max((t.turn_number for t in trace.turns), default=0)
|
|
@@ -175,6 +175,7 @@ class FindingKind(str, Enum):
|
|
|
175
175
|
FANOUT_WASTE = "fanout_waste"
|
|
176
176
|
PROJECT_PATTERN = "project_pattern"
|
|
177
177
|
CACHE_HYGIENE = "cache_hygiene"
|
|
178
|
+
COMPACTION = "compaction"
|
|
178
179
|
|
|
179
180
|
|
|
180
181
|
KIND_LABEL: dict[FindingKind, str] = {
|
|
@@ -186,6 +187,7 @@ KIND_LABEL: dict[FindingKind, str] = {
|
|
|
186
187
|
FindingKind.FANOUT_WASTE: "FANOUT WASTE",
|
|
187
188
|
FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
|
|
188
189
|
FindingKind.CACHE_HYGIENE: "CACHE HYGIENE",
|
|
190
|
+
FindingKind.COMPACTION: "COMPACTION",
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
# Maps FindingKind to the exact ## heading emitted by its recommender patch
|
|
@@ -200,6 +202,7 @@ MANAGED_HEADINGS: dict[FindingKind, str] = {
|
|
|
200
202
|
FindingKind.DEAD_END: "## Exploration discipline",
|
|
201
203
|
FindingKind.FANOUT_WASTE: "## Fan-out discipline",
|
|
202
204
|
FindingKind.CACHE_HYGIENE: "## Cache hygiene",
|
|
205
|
+
FindingKind.COMPACTION: "## Compaction hygiene",
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
# Project-specific patterns use a heading that embeds tool+key, so the managed
|
|
@@ -76,6 +76,15 @@ _CACHE_HYGIENE_DIFF = """\
|
|
|
76
76
|
+with a stable, cacheable preamble. A 10× cost difference separates a warm
|
|
77
77
|
+cache hit from a cold input read."""
|
|
78
78
|
|
|
79
|
+
_COMPACTION_DIFF = """\
|
|
80
|
+
+## Compaction hygiene
|
|
81
|
+
+
|
|
82
|
+
+If context-window compaction occurs mid-session, assume all previously read files
|
|
83
|
+
+are gone from context. Re-read only files you actively need for the next step —
|
|
84
|
+
+don't reflexively reload everything. Better: compact earlier by summarizing large
|
|
85
|
+
+tool outputs once you've extracted what you need, so compaction doesn't erase
|
|
86
|
+
+work-in-progress state."""
|
|
87
|
+
|
|
79
88
|
_TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
|
|
80
89
|
# kind → (description, diff_body, target_file)
|
|
81
90
|
FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
|
|
@@ -85,6 +94,7 @@ _TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
|
|
|
85
94
|
FindingKind.DEAD_END: ("Add exploration discipline rule", _DEAD_END_DIFF, "CLAUDE.md"),
|
|
86
95
|
FindingKind.FANOUT_WASTE: ("Add fan-out discipline rule", _FANOUT_WASTE_DIFF, "CLAUDE.md"),
|
|
87
96
|
FindingKind.CACHE_HYGIENE: ("Add cache hygiene rule", _CACHE_HYGIENE_DIFF, "CLAUDE.md"),
|
|
97
|
+
FindingKind.COMPACTION: ("Add compaction hygiene rule", _COMPACTION_DIFF, "CLAUDE.md"),
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
|
|
@@ -45,11 +45,35 @@ def _wide_console() -> Console:
|
|
|
45
45
|
return Console(width=200)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def compute_health_grade(diagnosis: Diagnosis) -> str:
|
|
49
|
+
"""A–F grade based on waste fraction and finding severity."""
|
|
50
|
+
if not diagnosis.findings:
|
|
51
|
+
return "A"
|
|
52
|
+
|
|
53
|
+
has_high = any(f.severity == Severity.HIGH for f in diagnosis.findings)
|
|
54
|
+
waste_frac = (
|
|
55
|
+
diagnosis.waste_cost_usd / diagnosis.total_cost_usd
|
|
56
|
+
if diagnosis.total_cost_usd > 0
|
|
57
|
+
else 0.0
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if has_high and waste_frac > 0.50:
|
|
61
|
+
return "F"
|
|
62
|
+
if has_high or waste_frac > 0.25:
|
|
63
|
+
return "D"
|
|
64
|
+
if waste_frac > 0.10:
|
|
65
|
+
return "C"
|
|
66
|
+
if diagnosis.findings:
|
|
67
|
+
return "B"
|
|
68
|
+
return "A"
|
|
69
|
+
|
|
70
|
+
|
|
48
71
|
def render_diagnosis(
|
|
49
72
|
diagnosis: Diagnosis,
|
|
50
73
|
*,
|
|
51
74
|
session_path: Path | None = None,
|
|
52
75
|
console: Console | None = None,
|
|
76
|
+
show_health: bool = False,
|
|
53
77
|
) -> None:
|
|
54
78
|
con = console or _default_console()
|
|
55
79
|
|
|
@@ -77,6 +101,15 @@ def render_diagnosis(
|
|
|
77
101
|
"~85–95% of actual billing; system framing not observable in JSONL", style="dim"
|
|
78
102
|
))
|
|
79
103
|
|
|
104
|
+
if show_health:
|
|
105
|
+
grade = compute_health_grade(diagnosis)
|
|
106
|
+
waste_frac = (
|
|
107
|
+
diagnosis.waste_cost_usd / diagnosis.total_cost_usd * 100
|
|
108
|
+
if diagnosis.total_cost_usd > 0
|
|
109
|
+
else 0.0
|
|
110
|
+
)
|
|
111
|
+
con.print(f"Health grade: {grade} (waste {waste_frac:.0f}% of session cost)")
|
|
112
|
+
|
|
80
113
|
if diagnosis.subagent_costs:
|
|
81
114
|
show_depth = any(a.depth > 1 for a in diagnosis.subagent_costs)
|
|
82
115
|
tbl = Table(show_header=True, header_style="bold", box=None, padding=(0, 1))
|
|
@@ -107,6 +140,8 @@ def render_diagnosis(
|
|
|
107
140
|
badge = Text(f" {label} ", style=style)
|
|
108
141
|
conf_note = f"({finding.confidence.value} confidence)"
|
|
109
142
|
con.print(badge, conf_note, "—", finding.summary)
|
|
143
|
+
if show_health and finding.cost_usd is not None:
|
|
144
|
+
con.print(f" → savings if fixed: ~${finding.cost_usd:.2f}")
|
|
110
145
|
|
|
111
146
|
# Patches
|
|
112
147
|
if diagnosis.patches:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cctx-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.15.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"
|