cctx-cli 1.7.0__tar.gz → 1.8.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.7.0 → cctx_cli-1.8.0}/CHANGELOG.md +30 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/PKG-INFO +1 -1
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/__init__.py +58 -3
- cctx_cli-1.8.0/cctx/diagnostician/patterns/fan_out.py +175 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/models.py +3 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/recommender/claude_md.py +10 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/pyproject.toml +1 -1
- cctx_cli-1.8.0/tests/test_fanout_classifier.py +344 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_harvest_emit.py +2 -1
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.gitignore +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/CLAUDE.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/DESIGN.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/PRODUCT.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/README.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/action.yml +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/agents.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/cli.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/harvest.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/terminal.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/demo.gif +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/demo.tape +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/conftest.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_diagnostician_subagents.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_models.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.8.0 (2026-06-11)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Add _FANOUT_WASTE_DIFF template so MANAGED_HEADINGS stays in sync
|
|
10
|
+
([#110](https://github.com/jacquardlabs/cctx/pull/110),
|
|
11
|
+
[`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
|
|
12
|
+
|
|
13
|
+
- Ruff lint — move imports to top, break long lines in test file
|
|
14
|
+
([#110](https://github.com/jacquardlabs/cctx/pull/110),
|
|
15
|
+
[`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- Add FindingKind.FANOUT_WASTE + KIND_LABEL + MANAGED_HEADINGS
|
|
20
|
+
([#110](https://github.com/jacquardlabs/cctx/pull/110),
|
|
21
|
+
[`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
|
|
22
|
+
|
|
23
|
+
- Fan-out waste classifier (M16 #89) ([#110](https://github.com/jacquardlabs/cctx/pull/110),
|
|
24
|
+
[`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
|
|
25
|
+
|
|
26
|
+
- Fan_out classifier — Signal A (overlap) + Signal B (retry)
|
|
27
|
+
([#110](https://github.com/jacquardlabs/cctx/pull/110),
|
|
28
|
+
[`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
|
|
29
|
+
|
|
30
|
+
- Wire fan_out classifier into diagnostician, add _patch_fanout_costs
|
|
31
|
+
([#110](https://github.com/jacquardlabs/cctx/pull/110),
|
|
32
|
+
[`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
|
|
33
|
+
|
|
34
|
+
|
|
5
35
|
## v1.7.0 (2026-06-11)
|
|
6
36
|
|
|
7
37
|
### Bug Fixes
|
|
@@ -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
|
dead_end,
|
|
19
|
+
fan_out,
|
|
19
20
|
retry_loop,
|
|
20
21
|
scope_creep,
|
|
21
22
|
stale_context,
|
|
@@ -41,6 +42,45 @@ def _patch_costs(findings: list[Finding], model: str | None) -> list[Finding]:
|
|
|
41
42
|
return result
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _patch_fanout_costs(
|
|
46
|
+
findings: list[Finding],
|
|
47
|
+
subagent_costs: list[SubagentAttribution],
|
|
48
|
+
) -> list[Finding]:
|
|
49
|
+
"""Fill cost_usd on FANOUT_WASTE findings from subagent attribution data.
|
|
50
|
+
|
|
51
|
+
For overlap findings: picks the cheaper of the two subagents as waste.
|
|
52
|
+
For retry findings: attributes the full cost of the failed subagent.
|
|
53
|
+
Populates evidence['subagent_session_ids'] so run()'s dedup pass works.
|
|
54
|
+
"""
|
|
55
|
+
cost_map = {a.session_id: a.total_cost_usd for a in subagent_costs}
|
|
56
|
+
result: list[Finding] = []
|
|
57
|
+
for f in findings:
|
|
58
|
+
if f.kind is FindingKind.FANOUT_WASTE:
|
|
59
|
+
signal = f.evidence.get("signal")
|
|
60
|
+
if signal == "overlap":
|
|
61
|
+
pair = [sid for sid in f.evidence.get("overlap_pair", []) if sid is not None]
|
|
62
|
+
if pair:
|
|
63
|
+
cheaper_cost, cheaper_sid = min(
|
|
64
|
+
(cost_map.get(sid, 0.0), sid) for sid in pair
|
|
65
|
+
)
|
|
66
|
+
f = dataclasses.replace(
|
|
67
|
+
f,
|
|
68
|
+
cost_usd=round(cheaper_cost, 4),
|
|
69
|
+
evidence={**f.evidence, "subagent_session_ids": [cheaper_sid]},
|
|
70
|
+
)
|
|
71
|
+
elif signal == "retry":
|
|
72
|
+
failed_sid = f.evidence.get("failed_session_id")
|
|
73
|
+
if failed_sid is not None:
|
|
74
|
+
cost = cost_map.get(failed_sid, 0.0)
|
|
75
|
+
f = dataclasses.replace(
|
|
76
|
+
f,
|
|
77
|
+
cost_usd=round(cost, 4),
|
|
78
|
+
evidence={**f.evidence, "subagent_session_ids": [failed_sid]},
|
|
79
|
+
)
|
|
80
|
+
result.append(f)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
44
84
|
def _compute_own_cost(trace: SessionTrace, model: str | None) -> float:
|
|
45
85
|
"""Parent-turns-only cost — does not recurse into subagents.
|
|
46
86
|
|
|
@@ -111,17 +151,32 @@ def run(trace: SessionTrace) -> Diagnosis:
|
|
|
111
151
|
*stale_context.classify(trace),
|
|
112
152
|
*tool_thrash.classify(trace),
|
|
113
153
|
*dead_end.classify(trace),
|
|
154
|
+
*fan_out.classify(trace),
|
|
114
155
|
]
|
|
115
156
|
findings.sort(key=lambda f: f.first_turn)
|
|
116
157
|
|
|
117
158
|
inflection_turn = inflection.detect(findings)
|
|
118
159
|
findings = _patch_costs(findings, trace.primary_model)
|
|
119
160
|
|
|
161
|
+
# Fan-out cost patching requires attributions first.
|
|
162
|
+
subagent_costs = _collect_attributions(trace)
|
|
163
|
+
findings = _patch_fanout_costs(findings, subagent_costs)
|
|
164
|
+
|
|
120
165
|
total_cost = round(_compute_inclusive_cost(trace), 4)
|
|
121
|
-
waste_cost = sum(f.cost_usd for f in findings if f.cost_usd is not None)
|
|
122
|
-
waste_cost = min(waste_cost, total_cost)
|
|
123
166
|
|
|
124
|
-
|
|
167
|
+
# Deduplicate fan-out waste: a subagent flagged by both overlap AND retry
|
|
168
|
+
# must not be double-counted. Collect unique wasted session IDs, sum once.
|
|
169
|
+
cost_map = {a.session_id: a.total_cost_usd for a in subagent_costs}
|
|
170
|
+
wasted_sids: set[str] = set()
|
|
171
|
+
for f in findings:
|
|
172
|
+
if f.kind is FindingKind.FANOUT_WASTE:
|
|
173
|
+
wasted_sids.update(f.evidence.get("subagent_session_ids", []))
|
|
174
|
+
fanout_waste = sum(cost_map.get(sid, 0.0) for sid in wasted_sids)
|
|
175
|
+
other_waste = sum(
|
|
176
|
+
f.cost_usd for f in findings
|
|
177
|
+
if f.cost_usd is not None and f.kind is not FindingKind.FANOUT_WASTE
|
|
178
|
+
)
|
|
179
|
+
waste_cost = min(other_waste + fanout_waste, total_cost)
|
|
125
180
|
|
|
126
181
|
return Diagnosis(
|
|
127
182
|
session_id=trace.session_id,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Fan-out waste classifier.
|
|
2
|
+
|
|
3
|
+
classify(trace) -> list[Finding]
|
|
4
|
+
|
|
5
|
+
Signal A — OVERLAP: Two Agent calls with Jaccard >= 0.65 on word 3-grams,
|
|
6
|
+
both prompts >= 50 words.
|
|
7
|
+
Signal B — RETRY: Agent ToolResult is_error=True followed by the next Agent
|
|
8
|
+
call with Jaccard >= 0.50 on word 3-grams, both prompts >= 30 words.
|
|
9
|
+
|
|
10
|
+
Signal C (unused-result) is deferred — the 6-gram approach fires false
|
|
11
|
+
positives on paraphrased references and is not ship-ready.
|
|
12
|
+
|
|
13
|
+
cost_usd is set to None here; _patch_fanout_costs() in diagnostician/__init__.py
|
|
14
|
+
fills it in from SubagentAttribution data after run() collects attributions.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
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, ToolUse
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Thresholds — documented here, not tuned at runtime
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
OVERLAP_JACCARD: float = 0.65 # minimum Jaccard on word 3-grams for overlap
|
|
30
|
+
OVERLAP_MIN_WORDS: int = 50 # both prompts must be this long
|
|
31
|
+
|
|
32
|
+
RETRY_JACCARD: float = 0.50 # minimum Jaccard for failed-retry detection
|
|
33
|
+
RETRY_MIN_WORDS: int = 30 # both prompts must be this long
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# N-gram helpers
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def _word_ngrams(text: str, n: int) -> set[tuple[str, ...]]:
|
|
41
|
+
words = text.lower().split()
|
|
42
|
+
if len(words) < n:
|
|
43
|
+
return set()
|
|
44
|
+
return {tuple(words[i : i + n]) for i in range(len(words) - n + 1)}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _jaccard(a: set, b: set) -> float:
|
|
48
|
+
if not a or not b:
|
|
49
|
+
return 0.0
|
|
50
|
+
return len(a & b) / len(a | b)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_prompt(tu: ToolUse) -> str:
|
|
54
|
+
return tu.tool_input.get("prompt") or tu.tool_input.get("description") or ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Signal A — Overlapping subagent prompts
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _signal_overlap(agent_calls: list[tuple[int, ToolUse]]) -> list[Finding]:
|
|
62
|
+
findings: list[Finding] = []
|
|
63
|
+
for i in range(len(agent_calls)):
|
|
64
|
+
turn_i, tu_i = agent_calls[i]
|
|
65
|
+
p_i = _get_prompt(tu_i)
|
|
66
|
+
words_i = p_i.split()
|
|
67
|
+
if len(words_i) < OVERLAP_MIN_WORDS:
|
|
68
|
+
continue
|
|
69
|
+
ng_i = _word_ngrams(p_i, 3)
|
|
70
|
+
for j in range(i + 1, len(agent_calls)):
|
|
71
|
+
turn_j, tu_j = agent_calls[j]
|
|
72
|
+
p_j = _get_prompt(tu_j)
|
|
73
|
+
words_j = p_j.split()
|
|
74
|
+
if len(words_j) < OVERLAP_MIN_WORDS:
|
|
75
|
+
continue
|
|
76
|
+
ng_j = _word_ngrams(p_j, 3)
|
|
77
|
+
score = _jaccard(ng_i, ng_j)
|
|
78
|
+
if score < OVERLAP_JACCARD:
|
|
79
|
+
continue
|
|
80
|
+
findings.append(Finding(
|
|
81
|
+
kind=FindingKind.FANOUT_WASTE,
|
|
82
|
+
severity=Severity.MEDIUM,
|
|
83
|
+
confidence=Confidence.MEDIUM,
|
|
84
|
+
first_turn=min(turn_i, turn_j),
|
|
85
|
+
last_turn=max(turn_i, turn_j),
|
|
86
|
+
evidence={
|
|
87
|
+
"signal": "overlap",
|
|
88
|
+
"overlap_pair": [tu_i.subagent_session_id, tu_j.subagent_session_id],
|
|
89
|
+
"jaccard": round(score, 3),
|
|
90
|
+
"prompt_a": p_i[:80],
|
|
91
|
+
"prompt_b": p_j[:80],
|
|
92
|
+
"subagent_session_ids": [], # filled by _patch_fanout_costs
|
|
93
|
+
},
|
|
94
|
+
cost_usd=None,
|
|
95
|
+
summary=f"Overlapping subagent prompts (Jaccard {score:.2f})",
|
|
96
|
+
))
|
|
97
|
+
return findings
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Signal B — Failed subagent re-spawned with similar prompt
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def _signal_retry(
|
|
105
|
+
agent_calls: list[tuple[int, ToolUse]],
|
|
106
|
+
result_map: dict[str, tuple[bool, str]], # tool_use_id -> (is_error, content)
|
|
107
|
+
) -> list[Finding]:
|
|
108
|
+
findings: list[Finding] = []
|
|
109
|
+
for k, (turn_k, tu_k) in enumerate(agent_calls):
|
|
110
|
+
is_error, _content = result_map.get(tu_k.tool_use_id, (False, ""))
|
|
111
|
+
if not is_error:
|
|
112
|
+
continue
|
|
113
|
+
# Only check the immediate next Agent call (by list order = turn order)
|
|
114
|
+
if k + 1 >= len(agent_calls):
|
|
115
|
+
continue
|
|
116
|
+
turn_next, tu_next = agent_calls[k + 1]
|
|
117
|
+
p_failed = _get_prompt(tu_k)
|
|
118
|
+
p_retry = _get_prompt(tu_next)
|
|
119
|
+
if len(p_failed.split()) < RETRY_MIN_WORDS or len(p_retry.split()) < RETRY_MIN_WORDS:
|
|
120
|
+
continue
|
|
121
|
+
score = _jaccard(_word_ngrams(p_failed, 3), _word_ngrams(p_retry, 3))
|
|
122
|
+
if score < RETRY_JACCARD:
|
|
123
|
+
continue
|
|
124
|
+
findings.append(Finding(
|
|
125
|
+
kind=FindingKind.FANOUT_WASTE,
|
|
126
|
+
severity=Severity.HIGH,
|
|
127
|
+
confidence=Confidence.HIGH,
|
|
128
|
+
first_turn=turn_k,
|
|
129
|
+
last_turn=turn_next,
|
|
130
|
+
evidence={
|
|
131
|
+
"signal": "retry",
|
|
132
|
+
"failed_session_id": tu_k.subagent_session_id,
|
|
133
|
+
"jaccard": round(score, 3),
|
|
134
|
+
"failed_prompt": p_failed[:80],
|
|
135
|
+
"retry_prompt": p_retry[:80],
|
|
136
|
+
"subagent_session_ids": [], # filled by _patch_fanout_costs
|
|
137
|
+
},
|
|
138
|
+
cost_usd=None,
|
|
139
|
+
summary=f"Failed subagent re-spawned with similar prompt (Jaccard {score:.2f})",
|
|
140
|
+
))
|
|
141
|
+
return findings
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Public entry point
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def _classify_impl(trace: SessionTrace) -> list[Finding]:
|
|
149
|
+
# Collect Agent ToolUse in turn order
|
|
150
|
+
agent_calls: list[tuple[int, ToolUse]] = []
|
|
151
|
+
result_map: dict[str, tuple[bool, str]] = {}
|
|
152
|
+
|
|
153
|
+
for turn in trace.turns:
|
|
154
|
+
for tu in turn.tool_uses:
|
|
155
|
+
if tu.tool_name == "Agent":
|
|
156
|
+
agent_calls.append((turn.turn_number, tu))
|
|
157
|
+
for tr in turn.tool_results:
|
|
158
|
+
if tr.tool_name == "Agent":
|
|
159
|
+
result_map[tr.tool_use_id] = (tr.is_error, tr.content)
|
|
160
|
+
|
|
161
|
+
if len(agent_calls) < 2:
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
findings: list[Finding] = [
|
|
165
|
+
*_signal_overlap(agent_calls),
|
|
166
|
+
*_signal_retry(agent_calls, result_map),
|
|
167
|
+
]
|
|
168
|
+
return findings
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def classify(trace: SessionTrace) -> list[Finding]:
|
|
172
|
+
try:
|
|
173
|
+
return _classify_impl(trace)
|
|
174
|
+
except Exception:
|
|
175
|
+
return []
|
|
@@ -172,6 +172,7 @@ class FindingKind(str, Enum):
|
|
|
172
172
|
STALE_CONTEXT = "stale_context"
|
|
173
173
|
TOOL_THRASH = "tool_thrash"
|
|
174
174
|
DEAD_END = "dead_end"
|
|
175
|
+
FANOUT_WASTE = "fanout_waste"
|
|
175
176
|
PROJECT_PATTERN = "project_pattern"
|
|
176
177
|
|
|
177
178
|
|
|
@@ -181,6 +182,7 @@ KIND_LABEL: dict[FindingKind, str] = {
|
|
|
181
182
|
FindingKind.STALE_CONTEXT: "STALE CONTEXT",
|
|
182
183
|
FindingKind.TOOL_THRASH: "TOOL THRASH",
|
|
183
184
|
FindingKind.DEAD_END: "DEAD END",
|
|
185
|
+
FindingKind.FANOUT_WASTE: "FANOUT WASTE",
|
|
184
186
|
FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
|
|
185
187
|
}
|
|
186
188
|
|
|
@@ -194,6 +196,7 @@ MANAGED_HEADINGS: dict[FindingKind, str] = {
|
|
|
194
196
|
FindingKind.STALE_CONTEXT: "## Context hygiene",
|
|
195
197
|
FindingKind.TOOL_THRASH: "## Tool-call discipline",
|
|
196
198
|
FindingKind.DEAD_END: "## Exploration discipline",
|
|
199
|
+
FindingKind.FANOUT_WASTE: "## Fan-out discipline",
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
# Project-specific patterns use a heading that embeds tool+key, so the managed
|
|
@@ -57,6 +57,15 @@ _DEAD_END_DIFF = """\
|
|
|
57
57
|
+approaches already ruled out, and pick a meaningfully different one. Sunk effort on a
|
|
58
58
|
+failing approach is not a reason to continue it."""
|
|
59
59
|
|
|
60
|
+
_FANOUT_WASTE_DIFF = """\
|
|
61
|
+
+## Fan-out discipline
|
|
62
|
+
+
|
|
63
|
+
+Before spawning multiple subagents in parallel, state what each one will return
|
|
64
|
+
+and verify the tasks don't overlap. After each subagent completes, confirm its
|
|
65
|
+
+result is actually consumed by the parent before spawning retries. Retry only
|
|
66
|
+
+after changing something meaningful about the task — identical re-spawns waste
|
|
67
|
+
+the full subagent cost with no new information."""
|
|
68
|
+
|
|
60
69
|
_TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
|
|
61
70
|
# kind → (description, diff_body, target_file)
|
|
62
71
|
FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
|
|
@@ -64,6 +73,7 @@ _TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
|
|
|
64
73
|
FindingKind.STALE_CONTEXT: ("Add context hygiene rule", _STALE_CONTEXT_DIFF, "CLAUDE.md"),
|
|
65
74
|
FindingKind.TOOL_THRASH: ("Add tool-call discipline rule", _TOOL_THRASH_DIFF, "CLAUDE.md"),
|
|
66
75
|
FindingKind.DEAD_END: ("Add exploration discipline rule", _DEAD_END_DIFF, "CLAUDE.md"),
|
|
76
|
+
FindingKind.FANOUT_WASTE: ("Add fan-out discipline rule", _FANOUT_WASTE_DIFF, "CLAUDE.md"),
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cctx-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.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"
|