cctx-cli 1.14.0__tar.gz → 1.16.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.14.0 → cctx_cli-1.16.0}/CHANGELOG.md +18 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/PKG-INFO +1 -1
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/__init__.py +4 -0
- cctx_cli-1.16.0/cctx/diagnostician/patterns/compaction.py +114 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/dead_end.py +2 -1
- cctx_cli-1.16.0/cctx/diagnostician/patterns/exploration_thrash.py +192 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/stale_context.py +4 -2
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/models.py +29 -23
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/recommender/claude_md.py +27 -5
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/pyproject.toml +1 -1
- cctx_cli-1.16.0/tests/test_compaction_classifier.py +373 -0
- cctx_cli-1.16.0/tests/test_exploration_thrash_classifier.py +272 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_harvest_emit.py +9 -7
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.gitignore +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/CLAUDE.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/DESIGN.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/PRODUCT.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/README.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/action.yml +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/agents.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/cli.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/cache_hygiene.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/fan_out.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/harvest.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/hook_installer.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/parsers/otel.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/terminal.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/demo.gif +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/demo.tape +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/quickstart-otel.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-06-19-otel-parser.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-06-19-otel-parser-design.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/conftest.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/otel_fanout.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/otel_handoff.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_cache_hygiene_classifier.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_diagnostician_subagents.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_efficacy.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_fanout_classifier.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_init.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_models.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_otel_parser.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_savings_framing.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.16.0 (2026-06-20)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Exploration thrash classifier — detect read-heavy circling without progress (#99)
|
|
10
|
+
([#124](https://github.com/jacquardlabs/cctx/pull/124),
|
|
11
|
+
[`21f3374`](https://github.com/jacquardlabs/cctx/commit/21f3374bb0e2c60db0411213773ad9dffca7ec1f))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## v1.15.0 (2026-06-20)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- Compaction findings — surface compaction events and re-fetch waste (#93)
|
|
19
|
+
([#123](https://github.com/jacquardlabs/cctx/pull/123),
|
|
20
|
+
[`cdab064`](https://github.com/jacquardlabs/cctx/commit/cdab0640b720e127549af63c22747555bd832b2c))
|
|
21
|
+
|
|
22
|
+
|
|
5
23
|
## v1.14.0 (2026-06-20)
|
|
6
24
|
|
|
7
25
|
### Features
|
|
@@ -16,7 +16,9 @@ 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,
|
|
21
|
+
exploration_thrash,
|
|
20
22
|
fan_out,
|
|
21
23
|
retry_loop,
|
|
22
24
|
scope_creep,
|
|
@@ -154,6 +156,8 @@ def run(trace: SessionTrace) -> Diagnosis:
|
|
|
154
156
|
*dead_end.classify(trace),
|
|
155
157
|
*fan_out.classify(trace),
|
|
156
158
|
*cache_hygiene.classify(trace),
|
|
159
|
+
*compaction.classify(trace),
|
|
160
|
+
*exploration_thrash.classify(trace),
|
|
157
161
|
]
|
|
158
162
|
findings.sort(key=lambda f: f.first_turn)
|
|
159
163
|
|
|
@@ -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
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Exploration-thrash classifier.
|
|
2
|
+
|
|
3
|
+
Detects when the assistant circles with repeated read/search tool calls
|
|
4
|
+
instead of making progress — high ratio of read-only tools to write/execute
|
|
5
|
+
tools in a window, or repeated identical discovery commands, with no file
|
|
6
|
+
edits or test runs in N consecutive turns.
|
|
7
|
+
|
|
8
|
+
Signals:
|
|
9
|
+
1. Sliding window of WINDOW_SIZE consecutive assistant turns where ≥ 80%
|
|
10
|
+
of tool calls are read-only and no Write/Edit appears.
|
|
11
|
+
2. Any (tool_name, key) read-only pair called ≥ REPEAT_THRESHOLD times
|
|
12
|
+
across the full session.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from collections import Counter
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from cctx.models import Confidence, Finding, FindingKind, Severity
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from cctx.models import SessionTrace
|
|
24
|
+
|
|
25
|
+
WINDOW_SIZE = 6
|
|
26
|
+
READ_RATIO_THRESHOLD = 0.80
|
|
27
|
+
REPEAT_THRESHOLD = 3
|
|
28
|
+
|
|
29
|
+
_READ_TOOLS = frozenset({"Read", "Grep", "Glob"})
|
|
30
|
+
_WRITE_TOOLS = frozenset({"Write", "Edit", "NotebookEdit"})
|
|
31
|
+
|
|
32
|
+
_READ_BASH_PREFIXES = (
|
|
33
|
+
"ls",
|
|
34
|
+
"find",
|
|
35
|
+
"cat",
|
|
36
|
+
"head",
|
|
37
|
+
"tail",
|
|
38
|
+
"grep",
|
|
39
|
+
"rg",
|
|
40
|
+
"ag",
|
|
41
|
+
"wc",
|
|
42
|
+
"file",
|
|
43
|
+
"stat",
|
|
44
|
+
"echo",
|
|
45
|
+
"pwd",
|
|
46
|
+
"which",
|
|
47
|
+
"type",
|
|
48
|
+
"less",
|
|
49
|
+
"git log",
|
|
50
|
+
"git diff",
|
|
51
|
+
"git show",
|
|
52
|
+
"git status",
|
|
53
|
+
"git blame",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_read_only(tool_name: str, tool_input: dict) -> bool:
|
|
58
|
+
if tool_name in _READ_TOOLS:
|
|
59
|
+
return True
|
|
60
|
+
if tool_name in _WRITE_TOOLS:
|
|
61
|
+
return False
|
|
62
|
+
if tool_name == "Bash":
|
|
63
|
+
cmd = tool_input.get("command", "").lstrip()
|
|
64
|
+
return any(cmd.startswith(p) for p in _READ_BASH_PREFIXES)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _tool_key(tool_name: str, tool_input: dict) -> str:
|
|
69
|
+
"""Canonical key for deduplication."""
|
|
70
|
+
match tool_name:
|
|
71
|
+
case "Bash":
|
|
72
|
+
return tool_input.get("command", "").strip()
|
|
73
|
+
case "Read" | "Write" | "Edit":
|
|
74
|
+
return tool_input.get("file_path", "")
|
|
75
|
+
case "Grep" | "Glob":
|
|
76
|
+
return tool_input.get("pattern", "")
|
|
77
|
+
case _:
|
|
78
|
+
return json.dumps(tool_input, sort_keys=True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _classify_impl(trace: SessionTrace) -> list[Finding]:
|
|
82
|
+
# Only look at assistant turns with tool calls
|
|
83
|
+
active_turns = [
|
|
84
|
+
t for t in trace.turns
|
|
85
|
+
if t.role == "assistant" and t.tool_uses
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
thrash_windows: list[dict] = []
|
|
89
|
+
|
|
90
|
+
# Sliding window detection — requires at least WINDOW_SIZE active turns
|
|
91
|
+
for i in range(max(0, len(active_turns) - WINDOW_SIZE + 1)):
|
|
92
|
+
window = active_turns[i : i + WINDOW_SIZE]
|
|
93
|
+
all_calls = [
|
|
94
|
+
(tu.tool_name, tu.tool_input)
|
|
95
|
+
for t in window
|
|
96
|
+
for tu in t.tool_uses
|
|
97
|
+
]
|
|
98
|
+
if not all_calls:
|
|
99
|
+
continue
|
|
100
|
+
read_count = sum(
|
|
101
|
+
1 for name, inp in all_calls if _is_read_only(name, inp)
|
|
102
|
+
)
|
|
103
|
+
ratio = read_count / len(all_calls)
|
|
104
|
+
has_write = any(name in _WRITE_TOOLS for name, _ in all_calls)
|
|
105
|
+
|
|
106
|
+
if ratio >= READ_RATIO_THRESHOLD and not has_write:
|
|
107
|
+
# Avoid double-counting overlapping windows that cover the same turns
|
|
108
|
+
if (
|
|
109
|
+
thrash_windows
|
|
110
|
+
and thrash_windows[-1]["last_turn"] >= window[0].turn_number
|
|
111
|
+
):
|
|
112
|
+
continue
|
|
113
|
+
thrash_windows.append({
|
|
114
|
+
"first_turn": window[0].turn_number,
|
|
115
|
+
"last_turn": window[-1].turn_number,
|
|
116
|
+
"read_ratio": round(ratio, 2),
|
|
117
|
+
"total_calls": len(all_calls),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
# Repeated identical reads
|
|
121
|
+
read_call_counts: Counter[str] = Counter()
|
|
122
|
+
read_call_turns: dict[str, list[int]] = {}
|
|
123
|
+
for turn in trace.turns:
|
|
124
|
+
if turn.role != "assistant":
|
|
125
|
+
continue
|
|
126
|
+
for tu in turn.tool_uses:
|
|
127
|
+
if _is_read_only(tu.tool_name, tu.tool_input):
|
|
128
|
+
key = f"{tu.tool_name}:{_tool_key(tu.tool_name, tu.tool_input)}"
|
|
129
|
+
read_call_counts[key] += 1
|
|
130
|
+
read_call_turns.setdefault(key, []).append(turn.turn_number)
|
|
131
|
+
|
|
132
|
+
repeated_reads: list[dict] = []
|
|
133
|
+
for key, count in read_call_counts.items():
|
|
134
|
+
if count >= REPEAT_THRESHOLD:
|
|
135
|
+
tool_name, _, call_key = key.partition(":")
|
|
136
|
+
repeated_reads.append({
|
|
137
|
+
"tool_name": tool_name,
|
|
138
|
+
"key": call_key[:60],
|
|
139
|
+
"count": count,
|
|
140
|
+
"turns": read_call_turns[key],
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
if not thrash_windows and not repeated_reads:
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
severity = Severity.HIGH if thrash_windows else Severity.MEDIUM
|
|
147
|
+
confidence = Confidence.MEDIUM
|
|
148
|
+
|
|
149
|
+
parts = []
|
|
150
|
+
if thrash_windows:
|
|
151
|
+
parts.append(
|
|
152
|
+
f"{len(thrash_windows)} exploration thrash window"
|
|
153
|
+
f"{'s' if len(thrash_windows) > 1 else ''} "
|
|
154
|
+
f"(turns {thrash_windows[0]['first_turn']}–{thrash_windows[-1]['last_turn']}, "
|
|
155
|
+
f"{thrash_windows[0]['read_ratio']:.0%} read-only)"
|
|
156
|
+
)
|
|
157
|
+
if repeated_reads:
|
|
158
|
+
worst = max(repeated_reads, key=lambda r: r["count"])
|
|
159
|
+
parts.append(
|
|
160
|
+
f"{worst['tool_name']}({worst['key']!r}) called {worst['count']}× identically"
|
|
161
|
+
)
|
|
162
|
+
summary = "; ".join(parts)
|
|
163
|
+
|
|
164
|
+
all_first = min(
|
|
165
|
+
[w["first_turn"] for w in thrash_windows]
|
|
166
|
+
+ [r["turns"][0] for r in repeated_reads]
|
|
167
|
+
)
|
|
168
|
+
all_last = max(
|
|
169
|
+
[w["last_turn"] for w in thrash_windows]
|
|
170
|
+
+ [r["turns"][-1] for r in repeated_reads]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return [Finding(
|
|
174
|
+
kind=FindingKind.EXPLORATION_THRASH,
|
|
175
|
+
severity=severity,
|
|
176
|
+
confidence=confidence,
|
|
177
|
+
first_turn=all_first,
|
|
178
|
+
last_turn=all_last,
|
|
179
|
+
evidence={
|
|
180
|
+
"thrash_windows": thrash_windows,
|
|
181
|
+
"repeated_reads": repeated_reads,
|
|
182
|
+
},
|
|
183
|
+
cost_usd=None,
|
|
184
|
+
summary=summary,
|
|
185
|
+
)]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def classify(trace: SessionTrace) -> list[Finding]:
|
|
189
|
+
try:
|
|
190
|
+
return _classify_impl(trace)
|
|
191
|
+
except Exception:
|
|
192
|
+
return []
|
|
@@ -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)
|
|
@@ -167,25 +167,29 @@ class SessionTrace:
|
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
class FindingKind(str, Enum):
|
|
170
|
-
RETRY_LOOP
|
|
171
|
-
SCOPE_CREEP
|
|
172
|
-
STALE_CONTEXT
|
|
173
|
-
TOOL_THRASH
|
|
174
|
-
DEAD_END
|
|
175
|
-
FANOUT_WASTE
|
|
176
|
-
PROJECT_PATTERN
|
|
177
|
-
CACHE_HYGIENE
|
|
170
|
+
RETRY_LOOP = "retry_loop"
|
|
171
|
+
SCOPE_CREEP = "scope_creep"
|
|
172
|
+
STALE_CONTEXT = "stale_context"
|
|
173
|
+
TOOL_THRASH = "tool_thrash"
|
|
174
|
+
DEAD_END = "dead_end"
|
|
175
|
+
FANOUT_WASTE = "fanout_waste"
|
|
176
|
+
PROJECT_PATTERN = "project_pattern"
|
|
177
|
+
CACHE_HYGIENE = "cache_hygiene"
|
|
178
|
+
COMPACTION = "compaction"
|
|
179
|
+
EXPLORATION_THRASH = "exploration_thrash"
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
KIND_LABEL: dict[FindingKind, str] = {
|
|
181
|
-
FindingKind.RETRY_LOOP:
|
|
182
|
-
FindingKind.SCOPE_CREEP:
|
|
183
|
-
FindingKind.STALE_CONTEXT:
|
|
184
|
-
FindingKind.TOOL_THRASH:
|
|
185
|
-
FindingKind.DEAD_END:
|
|
186
|
-
FindingKind.FANOUT_WASTE:
|
|
187
|
-
FindingKind.PROJECT_PATTERN:
|
|
188
|
-
FindingKind.CACHE_HYGIENE:
|
|
183
|
+
FindingKind.RETRY_LOOP: "RETRY LOOP",
|
|
184
|
+
FindingKind.SCOPE_CREEP: "SCOPE CREEP",
|
|
185
|
+
FindingKind.STALE_CONTEXT: "STALE CONTEXT",
|
|
186
|
+
FindingKind.TOOL_THRASH: "TOOL THRASH",
|
|
187
|
+
FindingKind.DEAD_END: "DEAD END",
|
|
188
|
+
FindingKind.FANOUT_WASTE: "FANOUT WASTE",
|
|
189
|
+
FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
|
|
190
|
+
FindingKind.CACHE_HYGIENE: "CACHE HYGIENE",
|
|
191
|
+
FindingKind.COMPACTION: "COMPACTION",
|
|
192
|
+
FindingKind.EXPLORATION_THRASH: "EXPLORATION THRASH",
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
# Maps FindingKind to the exact ## heading emitted by its recommender patch
|
|
@@ -193,13 +197,15 @@ KIND_LABEL: dict[FindingKind, str] = {
|
|
|
193
197
|
# harvest.py imports this (never reaches into recommender/) so emit/sync can
|
|
194
198
|
# identify cctx-managed sections without depending on the patch generator.
|
|
195
199
|
MANAGED_HEADINGS: dict[FindingKind, str] = {
|
|
196
|
-
FindingKind.RETRY_LOOP:
|
|
197
|
-
FindingKind.SCOPE_CREEP:
|
|
198
|
-
FindingKind.STALE_CONTEXT:
|
|
199
|
-
FindingKind.TOOL_THRASH:
|
|
200
|
-
FindingKind.DEAD_END:
|
|
201
|
-
FindingKind.FANOUT_WASTE:
|
|
202
|
-
FindingKind.CACHE_HYGIENE:
|
|
200
|
+
FindingKind.RETRY_LOOP: "## Retry discipline",
|
|
201
|
+
FindingKind.SCOPE_CREEP: "## Scope discipline",
|
|
202
|
+
FindingKind.STALE_CONTEXT: "## Context hygiene",
|
|
203
|
+
FindingKind.TOOL_THRASH: "## Tool-call discipline",
|
|
204
|
+
FindingKind.DEAD_END: "## Exploration discipline",
|
|
205
|
+
FindingKind.FANOUT_WASTE: "## Fan-out discipline",
|
|
206
|
+
FindingKind.CACHE_HYGIENE: "## Cache hygiene",
|
|
207
|
+
FindingKind.COMPACTION: "## Compaction hygiene",
|
|
208
|
+
FindingKind.EXPLORATION_THRASH: "## Exploration thrash",
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
# Project-specific patterns use a heading that embeds tool+key, so the managed
|
|
@@ -76,15 +76,37 @@ _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
|
+
|
|
88
|
+
_EXPLORATION_THRASH_DIFF = """\
|
|
89
|
+
+## Exploration thrash
|
|
90
|
+
+
|
|
91
|
+
+Before opening another file or running another search, state the specific
|
|
92
|
+
+question this call must answer and how the answer changes the next step. If
|
|
93
|
+
+you have read 6+ files without writing anything, stop and synthesise what
|
|
94
|
+
+you know. Never call the same read or grep more than twice with identical
|
|
95
|
+
+arguments — if the first two didn't help, a third won't either."""
|
|
96
|
+
|
|
79
97
|
_TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
|
|
80
98
|
# kind → (description, diff_body, target_file)
|
|
81
|
-
FindingKind.RETRY_LOOP:
|
|
82
|
-
FindingKind.SCOPE_CREEP:
|
|
99
|
+
FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
|
|
100
|
+
FindingKind.SCOPE_CREEP: ("Add scope discipline rule", _SCOPE_CREEP_DIFF, "CLAUDE.md"),
|
|
83
101
|
FindingKind.STALE_CONTEXT: ("Add context hygiene rule", _STALE_CONTEXT_DIFF, "CLAUDE.md"),
|
|
84
|
-
FindingKind.TOOL_THRASH:
|
|
85
|
-
FindingKind.DEAD_END:
|
|
86
|
-
FindingKind.FANOUT_WASTE:
|
|
102
|
+
FindingKind.TOOL_THRASH: ("Add tool-call discipline rule", _TOOL_THRASH_DIFF, "CLAUDE.md"),
|
|
103
|
+
FindingKind.DEAD_END: ("Add exploration discipline rule", _DEAD_END_DIFF, "CLAUDE.md"),
|
|
104
|
+
FindingKind.FANOUT_WASTE: ("Add fan-out discipline rule", _FANOUT_WASTE_DIFF, "CLAUDE.md"),
|
|
87
105
|
FindingKind.CACHE_HYGIENE: ("Add cache hygiene rule", _CACHE_HYGIENE_DIFF, "CLAUDE.md"),
|
|
106
|
+
FindingKind.COMPACTION: ("Add compaction hygiene rule", _COMPACTION_DIFF, "CLAUDE.md"),
|
|
107
|
+
FindingKind.EXPLORATION_THRASH: (
|
|
108
|
+
"Add exploration thrash rule", _EXPLORATION_THRASH_DIFF, "CLAUDE.md"
|
|
109
|
+
),
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cctx-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.16.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"
|