cctx-cli 1.2.0__tar.gz → 1.4.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.4.0/CHANGELOG.md +230 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/PKG-INFO +1 -1
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/cli.py +48 -10
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/aggregate.py +9 -6
- cctx_cli-1.4.0/cctx/diagnostician/patterns/project_specific.py +179 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/harvest.py +187 -18
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/models.py +15 -1
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/recommender/claude_md.py +25 -2
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/terminal.py +32 -12
- cctx_cli-1.4.0/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +860 -0
- cctx_cli-1.4.0/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +1312 -0
- cctx_cli-1.4.0/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +214 -0
- cctx_cli-1.4.0/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +235 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/pyproject.toml +1 -1
- cctx_cli-1.4.0/tests/diagnostician/test_project_specific.py +218 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_aggregate.py +6 -5
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_cli.py +129 -0
- cctx_cli-1.4.0/tests/test_harvest_check.py +418 -0
- cctx_cli-1.4.0/tests/test_models_project_pattern.py +37 -0
- cctx_cli-1.4.0/tests/test_recommender.py +56 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_terminal_renderer.py +74 -0
- cctx_cli-1.2.0/CHANGELOG.md +0 -106
- cctx_cli-1.2.0/tests/test_harvest_check.py +0 -139
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.gitignore +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/CLAUDE.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/DESIGN.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/PRODUCT.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/README.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/action.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/demo.gif +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/demo.tape +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/conftest.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_models.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_watcher.py +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
<!-- version list -->
|
|
4
|
+
|
|
5
|
+
## v1.4.0 (2026-05-20)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Deduplicate harvest check import, align severity badge output
|
|
10
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
11
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
12
|
+
|
|
13
|
+
- Move defaultdict import to top-level, add _words() return type
|
|
14
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
15
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
16
|
+
|
|
17
|
+
- Use removeprefix instead of lstrip to preserve .claude/skills/ dot prefix
|
|
18
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
19
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
- M15 harvest --check depth design spec ([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
24
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
25
|
+
|
|
26
|
+
- M15 harvest --check depth implementation plan
|
|
27
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
28
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
- --check-severity flag and severity badges in harvest --check output
|
|
33
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
34
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
35
|
+
|
|
36
|
+
- Check_contradictions() — always/never keyword heuristic
|
|
37
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
38
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
39
|
+
|
|
40
|
+
- Check_redundancy() — Jaccard similarity ≥ 0.8 on section word sets
|
|
41
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
42
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
43
|
+
|
|
44
|
+
- Check_staleness() — backtick function refs grepped against project source
|
|
45
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
46
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
47
|
+
|
|
48
|
+
- CheckSeverity enum, severity field on CheckFinding, new CheckIssue values
|
|
49
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
50
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
51
|
+
|
|
52
|
+
- Harvest --check depth — contradiction, redundancy, staleness detectors + --check-severity
|
|
53
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
54
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
55
|
+
|
|
56
|
+
- Wire all four detectors into check_claude_md ([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
57
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
58
|
+
|
|
59
|
+
### Refactoring
|
|
60
|
+
|
|
61
|
+
- Check_redundancy — compute _words once per section, remove dead union guard
|
|
62
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
63
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
64
|
+
|
|
65
|
+
- Check_staleness — module-level _STALENESS_EXCLUDED, min-len in regex, per-file search
|
|
66
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
67
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## v1.3.0 (2026-05-17)
|
|
71
|
+
|
|
72
|
+
### Bug Fixes
|
|
73
|
+
|
|
74
|
+
- Drop unused turn_number from result_map in _find_pairs
|
|
75
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
76
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
77
|
+
|
|
78
|
+
- Restore WHY comment, fix_key != failure_key guard, tighten tuple annotation
|
|
79
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
80
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
81
|
+
|
|
82
|
+
- Ruff lint failures (E501, F401, E741, I001) ([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
83
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
84
|
+
|
|
85
|
+
### Documentation
|
|
86
|
+
|
|
87
|
+
- M14 project-pattern-detection implementation plan
|
|
88
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
89
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
90
|
+
|
|
91
|
+
- M14 project-specific pattern detection design spec
|
|
92
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
93
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
94
|
+
|
|
95
|
+
- Note why harvest --since skips project_specific.detect()
|
|
96
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
97
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
98
|
+
|
|
99
|
+
### Features
|
|
100
|
+
|
|
101
|
+
- Add ProjectPattern model, AggregateReport.project_patterns, FindingKind.PROJECT_PATTERN
|
|
102
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
103
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
104
|
+
|
|
105
|
+
- Aggregate.run() returns (Diagnosis, SessionTrace) pairs
|
|
106
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
107
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
108
|
+
|
|
109
|
+
- Generate_from_patterns() — CLAUDE.md patches from ProjectPatterns
|
|
110
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
111
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
112
|
+
|
|
113
|
+
- M14 project-specific pattern detection ([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
114
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
115
|
+
|
|
116
|
+
- Project_specific.detect() — cross-session failure/fix pattern detector
|
|
117
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
118
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
119
|
+
|
|
120
|
+
- Render_aggregate() shows project-specific patterns table
|
|
121
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
122
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
123
|
+
|
|
124
|
+
- Wire project_specific.detect() into autopsy and harvest --since paths
|
|
125
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
126
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## v1.2.0 (2026-05-17)
|
|
130
|
+
|
|
131
|
+
### Features
|
|
132
|
+
|
|
133
|
+
- --until DATE, autopsy --json, export --format json (M12 #77 #78 #79)
|
|
134
|
+
([#84](https://github.com/jacquardlabs/cctx/pull/84),
|
|
135
|
+
[`803b5f1`](https://github.com/jacquardlabs/cctx/commit/803b5f190404679ddef4cbbec7478d04c57b8413))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## v1.1.0 (2026-05-17)
|
|
139
|
+
|
|
140
|
+
### Chores
|
|
141
|
+
|
|
142
|
+
- Add skip-existing to pypi publish action
|
|
143
|
+
([`23d7e16`](https://github.com/jacquardlabs/cctx/commit/23d7e16e18074da3c25899ba98298100ad3c1ad3))
|
|
144
|
+
|
|
145
|
+
### Features
|
|
146
|
+
|
|
147
|
+
- M9 polish — verdict headline, --top N, --turn N
|
|
148
|
+
([#83](https://github.com/jacquardlabs/cctx/pull/83),
|
|
149
|
+
[`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
|
|
150
|
+
|
|
151
|
+
- M9 polish — verdict headline, --top N, and --turn N
|
|
152
|
+
([#83](https://github.com/jacquardlabs/cctx/pull/83),
|
|
153
|
+
[`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
|
|
154
|
+
|
|
155
|
+
### Refactoring
|
|
156
|
+
|
|
157
|
+
- Cache verdict, fix markup=False bug, use reverse=True
|
|
158
|
+
([#83](https://github.com/jacquardlabs/cctx/pull/83),
|
|
159
|
+
[`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## v1.0.0 (2026-05-17)
|
|
163
|
+
|
|
164
|
+
### Continuous Integration
|
|
165
|
+
|
|
166
|
+
- Add python-semantic-release for fully automated CD
|
|
167
|
+
([`9844921`](https://github.com/jacquardlabs/cctx/commit/98449213e5b3bd597c47d54e4d5043e245adafe4))
|
|
168
|
+
|
|
169
|
+
- Add workflow_dispatch to release.yml for manual trigger
|
|
170
|
+
([`08ac9f8`](https://github.com/jacquardlabs/cctx/commit/08ac9f80eb6ceee7b57155852febc4274cbaf3b0))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## v0.2.0 (2026-05-16)
|
|
174
|
+
|
|
175
|
+
### Bug Fixes
|
|
176
|
+
|
|
177
|
+
- Ruff lint — B904, E402, E501, F841 across cli, tests, and renderers
|
|
178
|
+
([`fa7105f`](https://github.com/jacquardlabs/cctx/commit/fa7105fef340d89136f1996b86824e30d080a730))
|
|
179
|
+
|
|
180
|
+
- Trace TUI token sum within line-length limit
|
|
181
|
+
([`5b7416c`](https://github.com/jacquardlabs/cctx/commit/5b7416c5b92790cfe66f1e53f20891ecaf6e03b0))
|
|
182
|
+
|
|
183
|
+
### Chores
|
|
184
|
+
|
|
185
|
+
- Bump version to 0.2.0, update PRODUCT.md and CLAUDE.md
|
|
186
|
+
([`828ed49`](https://github.com/jacquardlabs/cctx/commit/828ed4997df9f4a264669bc38f4b10588a151f1c))
|
|
187
|
+
|
|
188
|
+
### Documentation
|
|
189
|
+
|
|
190
|
+
- Add CI usage section clarifying harvest is local-only
|
|
191
|
+
([`c526408`](https://github.com/jacquardlabs/cctx/commit/c526408a35749858e1c0b0b6ba42aea95bb8f621))
|
|
192
|
+
|
|
193
|
+
### Features
|
|
194
|
+
|
|
195
|
+
- **#64,#63**: Tool-thrash and dead-end exploration classifiers
|
|
196
|
+
([`14f8f45`](https://github.com/jacquardlabs/cctx/commit/14f8f45f9d3f4ef2fefac374d3e4cea36185c60d))
|
|
197
|
+
|
|
198
|
+
- **#65**: Harvest v2 — route patches to any .md target (.claude/rules/, .claude/skills/)
|
|
199
|
+
([`06ef9b7`](https://github.com/jacquardlabs/cctx/commit/06ef9b7a8ae9abacea54c2c826efc3fb6e6e80be))
|
|
200
|
+
|
|
201
|
+
- **#66**: Cctx harvest --check audits CLAUDE.md for dead refs and empty sections
|
|
202
|
+
([`a3be1d0`](https://github.com/jacquardlabs/cctx/commit/a3be1d0923d387b6830b10c7c2c5acf34a3b8917))
|
|
203
|
+
|
|
204
|
+
- **#67**: Interactive aggregate drill-down; --check docs in README
|
|
205
|
+
([`3db5429`](https://github.com/jacquardlabs/cctx/commit/3db5429dede40777b23d83e32a4b15a8c0e82a16))
|
|
206
|
+
|
|
207
|
+
- **#68**: --since accepts 7d, 2w, YYYY-MM-DD, and date ranges
|
|
208
|
+
([`434d7c4`](https://github.com/jacquardlabs/cctx/commit/434d7c448406e0a5465380ed395e1fadaa1c0db1))
|
|
209
|
+
|
|
210
|
+
- **#69**: Annotate costs as estimates (~85–95%) in terminal and HTML output
|
|
211
|
+
([`5a49889`](https://github.com/jacquardlabs/cctx/commit/5a49889bd43555d30a58c2b78eddf6acbb0d8e97))
|
|
212
|
+
|
|
213
|
+
- **#70**: Cctx watch — live waste signals during an active session
|
|
214
|
+
([`f533a13`](https://github.com/jacquardlabs/cctx/commit/f533a13e1f087da00457ce7b8215934f20c403a2))
|
|
215
|
+
|
|
216
|
+
- **#72**: Cctx autopsy --github-summary writes findings to GitHub Actions job summary
|
|
217
|
+
([`df91256`](https://github.com/jacquardlabs/cctx/commit/df91256b58b75a9bbe9594a000b00dee2ac2fbc5))
|
|
218
|
+
|
|
219
|
+
- **#73**: Cctx GitHub Action (composite) + --fail-on-findings flag
|
|
220
|
+
([`9229584`](https://github.com/jacquardlabs/cctx/commit/9229584ea192ce1b1ee6718721d981cba0ca13e0))
|
|
221
|
+
|
|
222
|
+
### Refactoring
|
|
223
|
+
|
|
224
|
+
- Consolidate KIND_LABEL, fix private import, clean up watcher tests
|
|
225
|
+
([`31ad4d7`](https://github.com/jacquardlabs/cctx/commit/31ad4d75885eed9c8238c4eb1c08bd5d1ba51a15))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
## v0.1.0 (2026-05-16)
|
|
229
|
+
|
|
230
|
+
- Initial Release
|
|
@@ -20,6 +20,7 @@ import rich_click as click
|
|
|
20
20
|
|
|
21
21
|
from cctx import diagnostician
|
|
22
22
|
from cctx.diagnostician import aggregate
|
|
23
|
+
from cctx.diagnostician.patterns import project_specific
|
|
23
24
|
from cctx.discovery import complete_project as _complete_project
|
|
24
25
|
from cctx.models import KIND_LABEL, AggregateReport
|
|
25
26
|
from cctx.parsers.claude_code import parse_session
|
|
@@ -146,22 +147,32 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
|
|
|
146
147
|
from rich.console import Console
|
|
147
148
|
from rich.rule import Rule
|
|
148
149
|
|
|
150
|
+
from cctx.harvest import CheckIssue, CheckSeverity
|
|
151
|
+
|
|
149
152
|
con = Console()
|
|
150
153
|
claude_md_path = target_dir / "CLAUDE.md"
|
|
151
154
|
con.print(Rule(f"cctx harvest --check — {claude_md_path}"))
|
|
152
155
|
if not findings:
|
|
153
|
-
con.print("✓ CLAUDE.md looks clean — no
|
|
156
|
+
con.print("✓ CLAUDE.md looks clean — no issues found.")
|
|
154
157
|
return
|
|
155
158
|
con.print(f"{len(findings)} issue(s) found:\n")
|
|
156
|
-
from cctx.harvest import CheckIssue
|
|
157
159
|
_ISSUE_LABEL = {
|
|
158
|
-
CheckIssue.DEAD_FILE_REF:
|
|
159
|
-
CheckIssue.DEAD_SKILL_REF:
|
|
160
|
-
CheckIssue.EMPTY_SECTION:
|
|
160
|
+
CheckIssue.DEAD_FILE_REF: "dead file reference",
|
|
161
|
+
CheckIssue.DEAD_SKILL_REF: "dead skill reference",
|
|
162
|
+
CheckIssue.EMPTY_SECTION: "empty section",
|
|
163
|
+
CheckIssue.CONTRADICTION: "contradiction",
|
|
164
|
+
CheckIssue.REDUNDANCY: "redundancy",
|
|
165
|
+
CheckIssue.STALE_IDENTIFIER: "stale identifier",
|
|
166
|
+
}
|
|
167
|
+
_SEV_BADGE = {
|
|
168
|
+
CheckSeverity.HIGH: "[HIGH]",
|
|
169
|
+
CheckSeverity.MEDIUM: "[MED]",
|
|
170
|
+
CheckSeverity.LOW: "[LOW]",
|
|
161
171
|
}
|
|
162
172
|
for f in findings:
|
|
173
|
+
badge = _SEV_BADGE.get(f.severity, " ")
|
|
163
174
|
label = _ISSUE_LABEL.get(f.issue, f.issue.value)
|
|
164
|
-
con.print(f"
|
|
175
|
+
con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
|
|
165
176
|
|
|
166
177
|
|
|
167
178
|
@click.group()
|
|
@@ -358,11 +369,14 @@ def autopsy(
|
|
|
358
369
|
f"Invalid --until date '{until_date}'. Expected YYYY-MM-DD."
|
|
359
370
|
) from None
|
|
360
371
|
label = f"{label} until {until_date.strip()}"
|
|
361
|
-
|
|
372
|
+
pairs = aggregate.run(project_dir, start, end)
|
|
373
|
+
diagnoses = [d for d, _ in pairs]
|
|
362
374
|
ev = evidence_mod.accumulate(diagnoses)
|
|
363
375
|
if top_n is not None:
|
|
364
376
|
ev = dict(sorted(ev.items(), key=lambda x: x[1].session_count, reverse=True)[:top_n])
|
|
365
|
-
|
|
377
|
+
patterns = project_specific.detect(pairs)
|
|
378
|
+
pattern_patches = claude_md.generate_from_patterns(patterns)
|
|
379
|
+
patches = claude_md.generate_from_evidence(ev) + pattern_patches
|
|
366
380
|
report = AggregateReport(
|
|
367
381
|
period_label=label,
|
|
368
382
|
sessions_analysed=len(diagnoses),
|
|
@@ -371,6 +385,7 @@ def autopsy(
|
|
|
371
385
|
waste_cost_usd=sum(d.waste_cost_usd for d in diagnoses),
|
|
372
386
|
by_kind=ev,
|
|
373
387
|
patches=patches,
|
|
388
|
+
project_patterns=patterns,
|
|
374
389
|
)
|
|
375
390
|
render_aggregate(report)
|
|
376
391
|
_aggregate_drilldown(report, diagnoses)
|
|
@@ -544,6 +559,14 @@ def trace(target: Path | None, latest: bool) -> None:
|
|
|
544
559
|
default=False,
|
|
545
560
|
help="Audit existing CLAUDE.md for dead references and empty sections. Exit 1 if findings.",
|
|
546
561
|
)
|
|
562
|
+
@click.option(
|
|
563
|
+
"--check-severity",
|
|
564
|
+
"check_severity",
|
|
565
|
+
default="MEDIUM",
|
|
566
|
+
type=click.Choice(["LOW", "MEDIUM", "HIGH"], case_sensitive=False),
|
|
567
|
+
show_default=True,
|
|
568
|
+
help="Minimum severity that causes --check to exit 1.",
|
|
569
|
+
)
|
|
547
570
|
def harvest(
|
|
548
571
|
target: Path,
|
|
549
572
|
since: str | None,
|
|
@@ -551,15 +574,27 @@ def harvest(
|
|
|
551
574
|
dry_run: bool,
|
|
552
575
|
target_dir: Path | None,
|
|
553
576
|
check_mode: bool,
|
|
577
|
+
check_severity: str,
|
|
554
578
|
) -> None:
|
|
555
579
|
"""Apply autopsy patches to CLAUDE.md."""
|
|
556
580
|
from cctx.harvest import apply_patches, check_claude_md, preview_patches
|
|
557
581
|
|
|
558
582
|
if check_mode:
|
|
583
|
+
from cctx.harvest import CheckSeverity
|
|
559
584
|
resolved_dir = target_dir or Path.cwd()
|
|
560
585
|
findings = check_claude_md(resolved_dir)
|
|
561
586
|
_render_check_findings(findings, resolved_dir)
|
|
562
|
-
|
|
587
|
+
_SEVERITY_ORDER = {
|
|
588
|
+
CheckSeverity.LOW: 0,
|
|
589
|
+
CheckSeverity.MEDIUM: 1,
|
|
590
|
+
CheckSeverity.HIGH: 2,
|
|
591
|
+
}
|
|
592
|
+
threshold = CheckSeverity(check_severity.lower())
|
|
593
|
+
triggering = [
|
|
594
|
+
f for f in findings
|
|
595
|
+
if _SEVERITY_ORDER[f.severity] >= _SEVERITY_ORDER[threshold]
|
|
596
|
+
]
|
|
597
|
+
raise SystemExit(1 if triggering else 0)
|
|
563
598
|
|
|
564
599
|
if apply_mode and dry_run:
|
|
565
600
|
raise click.UsageError("--apply and --dry-run are mutually exclusive.")
|
|
@@ -571,8 +606,11 @@ def harvest(
|
|
|
571
606
|
if since is not None:
|
|
572
607
|
project_dir = target if target.is_dir() else target.parent
|
|
573
608
|
start, end, _label = parse_since(since)
|
|
574
|
-
|
|
609
|
+
pairs = aggregate.run(project_dir, start, end)
|
|
610
|
+
diagnoses = [d for d, _ in pairs]
|
|
575
611
|
ev = evidence_mod.accumulate(diagnoses)
|
|
612
|
+
# project_specific.detect() intentionally omitted: pattern patches need human review
|
|
613
|
+
# (autopsy shows them; harvest doesn't auto-apply).
|
|
576
614
|
patches = claude_md.generate_from_evidence(ev)
|
|
577
615
|
else:
|
|
578
616
|
if target.is_dir():
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Cross-session aggregator.
|
|
2
2
|
|
|
3
|
-
run(project_dir, start, end) -> list[Diagnosis]
|
|
3
|
+
run(project_dir, start, end) -> list[tuple[Diagnosis, SessionTrace]]
|
|
4
4
|
|
|
5
5
|
Discovers session JSONL files in project_dir modified within [start, end],
|
|
6
|
-
parses each one, runs the per-session diagnostician, and returns
|
|
7
|
-
|
|
6
|
+
parses each one, runs the per-session diagnostician, and returns
|
|
7
|
+
(Diagnosis, SessionTrace) pairs. The CLI orchestrates recommender and
|
|
8
|
+
project-specific detection separately.
|
|
8
9
|
"""
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
@@ -17,12 +18,14 @@ from cctx.parsers.claude_code import parse_session
|
|
|
17
18
|
from cctx.tokenizer import tokenize_session
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
|
-
from cctx.models import Diagnosis
|
|
21
|
+
from cctx.models import Diagnosis, SessionTrace
|
|
21
22
|
|
|
22
23
|
UTC = timezone.utc
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def run(
|
|
26
|
+
def run(
|
|
27
|
+
project_dir: Path, start: datetime, end: datetime
|
|
28
|
+
) -> list[tuple[Diagnosis, SessionTrace]]:
|
|
26
29
|
paths = sorted(project_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime)
|
|
27
30
|
|
|
28
31
|
result = []
|
|
@@ -33,7 +36,7 @@ def run(project_dir: Path, start: datetime, end: datetime) -> list[Diagnosis]:
|
|
|
33
36
|
try:
|
|
34
37
|
trace = tokenize_session(parse_session(path))
|
|
35
38
|
diagnosis = diagnostician.run(trace)
|
|
36
|
-
result.append(diagnosis)
|
|
39
|
+
result.append((diagnosis, trace))
|
|
37
40
|
except Exception:
|
|
38
41
|
continue # skip corrupt sessions; don't fail the whole run
|
|
39
42
|
return result
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Project-specific pattern detector.
|
|
2
|
+
|
|
3
|
+
detect(pairs) -> list[ProjectPattern]
|
|
4
|
+
|
|
5
|
+
Finds (tool_name, failure_key, fix_key) triples that recur in 3+ sessions.
|
|
6
|
+
Bash normalization uses first 3 tokens for cross-session fuzzy matching
|
|
7
|
+
(intentionally looser than retry_loop). No LLM calls.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from cctx.models import ProjectPattern
|
|
16
|
+
from cctx.pricing import price_per_tok
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from cctx.models import Diagnosis, SessionTrace, ToolResult
|
|
20
|
+
|
|
21
|
+
MIN_SESSIONS = 3
|
|
22
|
+
FIX_WINDOW = 10 # turns after last failure to search for the fix
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalize_key(tool_name: str, tool_input: dict) -> str:
|
|
26
|
+
match tool_name:
|
|
27
|
+
case "Bash":
|
|
28
|
+
tokens = tool_input.get("command", "").strip().split()
|
|
29
|
+
return " ".join(tokens[:3])
|
|
30
|
+
case "Edit" | "Read" | "Write":
|
|
31
|
+
return tool_input.get("file_path", "")
|
|
32
|
+
case "Grep" | "Glob":
|
|
33
|
+
return tool_input.get("pattern", "")
|
|
34
|
+
case _:
|
|
35
|
+
return json.dumps(tool_input, sort_keys=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_error(result: ToolResult) -> bool:
|
|
39
|
+
if result.is_error:
|
|
40
|
+
return True
|
|
41
|
+
c = result.content
|
|
42
|
+
return c.startswith("Error:") or c.startswith("error:") or c.startswith("FAILED")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _find_pairs(trace: SessionTrace) -> list[dict]:
|
|
46
|
+
"""Find failure/fix pairs within one session."""
|
|
47
|
+
result_map: dict[str, ToolResult] = {}
|
|
48
|
+
for turn in trace.turns:
|
|
49
|
+
for tr in turn.tool_results:
|
|
50
|
+
result_map[tr.tool_use_id] = tr
|
|
51
|
+
|
|
52
|
+
records = []
|
|
53
|
+
for turn in trace.turns:
|
|
54
|
+
if turn.role != "assistant":
|
|
55
|
+
continue
|
|
56
|
+
for tu in turn.tool_uses:
|
|
57
|
+
result = result_map.get(tu.tool_use_id)
|
|
58
|
+
if result is None:
|
|
59
|
+
continue
|
|
60
|
+
key = _normalize_key(tu.tool_name, tu.tool_input)
|
|
61
|
+
records.append({
|
|
62
|
+
"tool_name": tu.tool_name,
|
|
63
|
+
"key": key,
|
|
64
|
+
"turn": turn.turn_number,
|
|
65
|
+
"is_error": _is_error(result),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
groups: dict[tuple, list] = defaultdict(list)
|
|
69
|
+
for r in records:
|
|
70
|
+
groups[(r["tool_name"], r["key"])].append(r)
|
|
71
|
+
|
|
72
|
+
found: list[dict] = []
|
|
73
|
+
seen_pairs: set[tuple] = set()
|
|
74
|
+
|
|
75
|
+
for (tool_name, failure_key), group in groups.items():
|
|
76
|
+
errors = [r for r in group if r["is_error"]]
|
|
77
|
+
if len(errors) < 2:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
first_err_turn = errors[0]["turn"]
|
|
81
|
+
last_err_turn = errors[-1]["turn"]
|
|
82
|
+
|
|
83
|
+
intervening = any(
|
|
84
|
+
r for r in group
|
|
85
|
+
if not r["is_error"] and first_err_turn < r["turn"] < last_err_turn
|
|
86
|
+
)
|
|
87
|
+
if intervening:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
fix = next(
|
|
91
|
+
(
|
|
92
|
+
r for r in records
|
|
93
|
+
if r["tool_name"] == tool_name
|
|
94
|
+
and not r["is_error"]
|
|
95
|
+
and r["key"] != failure_key
|
|
96
|
+
and last_err_turn < r["turn"] <= last_err_turn + FIX_WINDOW
|
|
97
|
+
),
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
if fix is None:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
pair_key = (tool_name, failure_key, fix["key"])
|
|
104
|
+
if pair_key in seen_pairs:
|
|
105
|
+
continue
|
|
106
|
+
seen_pairs.add(pair_key)
|
|
107
|
+
|
|
108
|
+
found.append({
|
|
109
|
+
"tool_name": tool_name,
|
|
110
|
+
"failure_key": failure_key,
|
|
111
|
+
"fix_key": fix["key"],
|
|
112
|
+
"first_failure_turn": first_err_turn,
|
|
113
|
+
"fix_turn": fix["turn"],
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return found
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _compute_waste(trace: SessionTrace, first_failure_turn: int, fix_turn: int) -> float:
|
|
120
|
+
price = price_per_tok(trace.primary_model)
|
|
121
|
+
total = 0.0
|
|
122
|
+
for turn in trace.turns:
|
|
123
|
+
if (
|
|
124
|
+
turn.role == "assistant"
|
|
125
|
+
and first_failure_turn <= turn.turn_number <= fix_turn
|
|
126
|
+
and turn.usage is not None
|
|
127
|
+
):
|
|
128
|
+
total += turn.usage.input_tokens * price
|
|
129
|
+
return round(total, 4)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def detect(pairs: list[tuple[Diagnosis, SessionTrace]]) -> list[ProjectPattern]:
|
|
133
|
+
"""Detect recurring failure/fix patterns across sessions."""
|
|
134
|
+
session_records: list[dict] = []
|
|
135
|
+
for _diagnosis, trace in pairs:
|
|
136
|
+
for p in _find_pairs(trace):
|
|
137
|
+
session_records.append({
|
|
138
|
+
"session_id": trace.session_id,
|
|
139
|
+
"tool_name": p["tool_name"],
|
|
140
|
+
"failure_key": p["failure_key"],
|
|
141
|
+
"fix_key": p["fix_key"],
|
|
142
|
+
"first_failure_turn": p["first_failure_turn"],
|
|
143
|
+
"fix_turn": p["fix_turn"],
|
|
144
|
+
"trace": trace,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
groups: dict[tuple, list] = defaultdict(list)
|
|
148
|
+
for r in session_records:
|
|
149
|
+
groups[(r["tool_name"], r["failure_key"], r["fix_key"])].append(r)
|
|
150
|
+
|
|
151
|
+
result: list[ProjectPattern] = []
|
|
152
|
+
for (tool_name, failure_key, fix_key), records in groups.items():
|
|
153
|
+
seen: dict[str, dict] = {}
|
|
154
|
+
for r in records:
|
|
155
|
+
if r["session_id"] not in seen:
|
|
156
|
+
seen[r["session_id"]] = r
|
|
157
|
+
|
|
158
|
+
if len(seen) < MIN_SESSIONS:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
unique = list(seen.values())
|
|
162
|
+
wasted = [r["fix_turn"] - r["first_failure_turn"] for r in unique]
|
|
163
|
+
avg_wasted_turns = sum(wasted) / len(wasted)
|
|
164
|
+
total_waste_usd = sum(
|
|
165
|
+
_compute_waste(r["trace"], r["first_failure_turn"], r["fix_turn"])
|
|
166
|
+
for r in unique
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
result.append(ProjectPattern(
|
|
170
|
+
tool_name=tool_name,
|
|
171
|
+
failure_key=failure_key,
|
|
172
|
+
fix_key=fix_key,
|
|
173
|
+
session_count=len(seen),
|
|
174
|
+
avg_wasted_turns=round(avg_wasted_turns, 1),
|
|
175
|
+
total_waste_usd=round(total_waste_usd, 4),
|
|
176
|
+
example_sessions=sorted(r["session_id"] for r in unique)[:3],
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
return result
|