cctx-cli 1.2.0__tar.gz → 1.3.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.2.0 → cctx_cli-1.3.0}/CHANGELOG.md +59 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/PKG-INFO +1 -1
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/cli.py +11 -3
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/aggregate.py +9 -6
- cctx_cli-1.3.0/cctx/diagnostician/patterns/project_specific.py +179 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/models.py +15 -1
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/recommender/claude_md.py +25 -2
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/terminal.py +32 -12
- cctx_cli-1.3.0/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +1312 -0
- cctx_cli-1.3.0/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +235 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/pyproject.toml +1 -1
- cctx_cli-1.3.0/tests/diagnostician/test_project_specific.py +218 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_aggregate.py +6 -5
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_cli.py +129 -0
- cctx_cli-1.3.0/tests/test_models_project_pattern.py +37 -0
- cctx_cli-1.3.0/tests/test_recommender.py +56 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_terminal_renderer.py +74 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.gitignore +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/CLAUDE.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/DESIGN.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/PRODUCT.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/README.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/action.yml +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/harvest.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/demo.gif +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/demo.tape +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/conftest.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.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.3.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_models.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.3.0 (2026-05-17)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Drop unused turn_number from result_map in _find_pairs
|
|
10
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
11
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
12
|
+
|
|
13
|
+
- Restore WHY comment, fix_key != failure_key guard, tighten tuple annotation
|
|
14
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
15
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
16
|
+
|
|
17
|
+
- Ruff lint failures (E501, F401, E741, I001) ([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
18
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- M14 project-pattern-detection implementation plan
|
|
23
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
24
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
25
|
+
|
|
26
|
+
- M14 project-specific pattern detection design spec
|
|
27
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
28
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
29
|
+
|
|
30
|
+
- Note why harvest --since skips project_specific.detect()
|
|
31
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
32
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
- Add ProjectPattern model, AggregateReport.project_patterns, FindingKind.PROJECT_PATTERN
|
|
37
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
38
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
39
|
+
|
|
40
|
+
- Aggregate.run() returns (Diagnosis, SessionTrace) pairs
|
|
41
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
42
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
43
|
+
|
|
44
|
+
- Generate_from_patterns() — CLAUDE.md patches from ProjectPatterns
|
|
45
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
46
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
47
|
+
|
|
48
|
+
- M14 project-specific pattern detection ([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
49
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
50
|
+
|
|
51
|
+
- Project_specific.detect() — cross-session failure/fix pattern detector
|
|
52
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
53
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
54
|
+
|
|
55
|
+
- Render_aggregate() shows project-specific patterns table
|
|
56
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
57
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
58
|
+
|
|
59
|
+
- Wire project_specific.detect() into autopsy and harvest --since paths
|
|
60
|
+
([#86](https://github.com/jacquardlabs/cctx/pull/86),
|
|
61
|
+
[`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
|
|
62
|
+
|
|
63
|
+
|
|
5
64
|
## v1.2.0 (2026-05-17)
|
|
6
65
|
|
|
7
66
|
### Features
|
|
@@ -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
|
|
@@ -358,11 +359,14 @@ def autopsy(
|
|
|
358
359
|
f"Invalid --until date '{until_date}'. Expected YYYY-MM-DD."
|
|
359
360
|
) from None
|
|
360
361
|
label = f"{label} until {until_date.strip()}"
|
|
361
|
-
|
|
362
|
+
pairs = aggregate.run(project_dir, start, end)
|
|
363
|
+
diagnoses = [d for d, _ in pairs]
|
|
362
364
|
ev = evidence_mod.accumulate(diagnoses)
|
|
363
365
|
if top_n is not None:
|
|
364
366
|
ev = dict(sorted(ev.items(), key=lambda x: x[1].session_count, reverse=True)[:top_n])
|
|
365
|
-
|
|
367
|
+
patterns = project_specific.detect(pairs)
|
|
368
|
+
pattern_patches = claude_md.generate_from_patterns(patterns)
|
|
369
|
+
patches = claude_md.generate_from_evidence(ev) + pattern_patches
|
|
366
370
|
report = AggregateReport(
|
|
367
371
|
period_label=label,
|
|
368
372
|
sessions_analysed=len(diagnoses),
|
|
@@ -371,6 +375,7 @@ def autopsy(
|
|
|
371
375
|
waste_cost_usd=sum(d.waste_cost_usd for d in diagnoses),
|
|
372
376
|
by_kind=ev,
|
|
373
377
|
patches=patches,
|
|
378
|
+
project_patterns=patterns,
|
|
374
379
|
)
|
|
375
380
|
render_aggregate(report)
|
|
376
381
|
_aggregate_drilldown(report, diagnoses)
|
|
@@ -571,8 +576,11 @@ def harvest(
|
|
|
571
576
|
if since is not None:
|
|
572
577
|
project_dir = target if target.is_dir() else target.parent
|
|
573
578
|
start, end, _label = parse_since(since)
|
|
574
|
-
|
|
579
|
+
pairs = aggregate.run(project_dir, start, end)
|
|
580
|
+
diagnoses = [d for d, _ in pairs]
|
|
575
581
|
ev = evidence_mod.accumulate(diagnoses)
|
|
582
|
+
# project_specific.detect() intentionally omitted: pattern patches need human review
|
|
583
|
+
# (autopsy shows them; harvest doesn't auto-apply).
|
|
576
584
|
patches = claude_md.generate_from_evidence(ev)
|
|
577
585
|
else:
|
|
578
586
|
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
|
|
@@ -9,7 +9,7 @@ cctx.renderers, cctx.exporters, cctx.tokenizer.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from dataclasses import dataclass
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
13
|
from datetime import datetime
|
|
14
14
|
from enum import Enum
|
|
15
15
|
from pathlib import Path
|
|
@@ -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
|
+
PROJECT_PATTERN = "project_pattern"
|
|
175
176
|
|
|
176
177
|
|
|
177
178
|
KIND_LABEL: dict[FindingKind, str] = {
|
|
@@ -180,6 +181,7 @@ KIND_LABEL: dict[FindingKind, str] = {
|
|
|
180
181
|
FindingKind.STALE_CONTEXT: "STALE CONTEXT",
|
|
181
182
|
FindingKind.TOOL_THRASH: "TOOL THRASH",
|
|
182
183
|
FindingKind.DEAD_END: "DEAD END",
|
|
184
|
+
FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
|
|
@@ -241,6 +243,17 @@ class KindEvidence:
|
|
|
241
243
|
example_summaries: list[str]
|
|
242
244
|
|
|
243
245
|
|
|
246
|
+
@dataclass
|
|
247
|
+
class ProjectPattern:
|
|
248
|
+
tool_name: str
|
|
249
|
+
failure_key: str
|
|
250
|
+
fix_key: str
|
|
251
|
+
session_count: int
|
|
252
|
+
avg_wasted_turns: float
|
|
253
|
+
total_waste_usd: float
|
|
254
|
+
example_sessions: list[str]
|
|
255
|
+
|
|
256
|
+
|
|
244
257
|
@dataclass
|
|
245
258
|
class AggregateReport:
|
|
246
259
|
period_label: str # human-readable, e.g. "last 7 days" or "2026-05-01..2026-05-15"
|
|
@@ -250,6 +263,7 @@ class AggregateReport:
|
|
|
250
263
|
waste_cost_usd: float
|
|
251
264
|
by_kind: dict[FindingKind, KindEvidence]
|
|
252
265
|
patches: list[Patch]
|
|
266
|
+
project_patterns: list[ProjectPattern] = field(default_factory=list)
|
|
253
267
|
|
|
254
268
|
|
|
255
269
|
# ---------------------------------------------------------------------------
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Patch generator — turns Findings into copy-pasteable CLAUDE.md diffs.
|
|
2
2
|
|
|
3
3
|
generate(diagnosis) -> Diagnosis (single-session path)
|
|
4
|
-
generate_from_evidence(evidence) -> list[Patch] (cross-session path)
|
|
4
|
+
generate_from_evidence(evidence) -> list[Patch] (cross-session path, generic findings)
|
|
5
|
+
generate_from_patterns(patterns) -> list[Patch] (cross-session path, project patterns)
|
|
5
6
|
"""
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
@@ -11,7 +12,7 @@ from typing import TYPE_CHECKING
|
|
|
11
12
|
from cctx.models import FindingKind, Patch
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
|
-
from cctx.models import Diagnosis, Finding, KindEvidence
|
|
15
|
+
from cctx.models import Diagnosis, Finding, KindEvidence, ProjectPattern
|
|
15
16
|
|
|
16
17
|
# ---------------------------------------------------------------------------
|
|
17
18
|
# Patch templates (append-style unified diffs, v0)
|
|
@@ -129,3 +130,25 @@ def generate_from_evidence(
|
|
|
129
130
|
evidence_summary=example,
|
|
130
131
|
))
|
|
131
132
|
return patches
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def generate_from_patterns(patterns: list[ProjectPattern]) -> list[Patch]:
|
|
136
|
+
"""Generate CLAUDE.md patches from cross-session ProjectPatterns."""
|
|
137
|
+
patches = []
|
|
138
|
+
for p in patterns:
|
|
139
|
+
diff = (
|
|
140
|
+
f"+## Project-specific: {p.tool_name}({p.failure_key})\n"
|
|
141
|
+
f"+When `{p.failure_key}` fails, use `{p.fix_key}` instead.\n"
|
|
142
|
+
f"+Re-discovered in {p.session_count} sessions "
|
|
143
|
+
f"(~${p.total_waste_usd:.2f} wasted)."
|
|
144
|
+
)
|
|
145
|
+
patches.append(Patch(
|
|
146
|
+
target_file="CLAUDE.md",
|
|
147
|
+
description=f"Project-specific: {p.failure_key} → {p.fix_key}",
|
|
148
|
+
unified_diff=diff,
|
|
149
|
+
finding_kind=FindingKind.PROJECT_PATTERN,
|
|
150
|
+
evidence_summary=(
|
|
151
|
+
f"Seen in {p.session_count} sessions, ~${p.total_waste_usd:.2f} wasted"
|
|
152
|
+
),
|
|
153
|
+
))
|
|
154
|
+
return patches
|
|
@@ -112,22 +112,23 @@ def render_aggregate(report: AggregateReport, *, console: Console | None = None)
|
|
|
112
112
|
f"Waste: ${report.waste_cost_usd:.2f}"
|
|
113
113
|
)
|
|
114
114
|
|
|
115
|
-
if not report.by_kind:
|
|
115
|
+
if not report.by_kind and not report.project_patterns:
|
|
116
116
|
con.print("\nNo findings across sessions.")
|
|
117
117
|
return
|
|
118
118
|
|
|
119
119
|
# Summary table
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
if report.by_kind:
|
|
121
|
+
table = Table(title="Finding frequency")
|
|
122
|
+
table.add_column("Pattern")
|
|
123
|
+
table.add_column("Sessions", justify="right")
|
|
124
|
+
table.add_column("Waste ($)", justify="right")
|
|
125
|
+
for kind, ev in report.by_kind.items():
|
|
126
|
+
table.add_row(
|
|
127
|
+
_KIND_LABEL.get(kind, kind.value),
|
|
128
|
+
str(ev.session_count),
|
|
129
|
+
f"${ev.total_waste_usd:.2f}",
|
|
130
|
+
)
|
|
131
|
+
con.print(table)
|
|
131
132
|
|
|
132
133
|
# Patches
|
|
133
134
|
if report.patches:
|
|
@@ -137,6 +138,25 @@ def render_aggregate(report: AggregateReport, *, console: Console | None = None)
|
|
|
137
138
|
syntax = Syntax(patch.unified_diff, "diff", theme="monokai", word_wrap=True)
|
|
138
139
|
con.print(syntax)
|
|
139
140
|
|
|
141
|
+
# Project-specific patterns
|
|
142
|
+
if report.project_patterns:
|
|
143
|
+
con.print()
|
|
144
|
+
pp_table = Table(title="Project-specific patterns")
|
|
145
|
+
pp_table.add_column("Failure", style="bold")
|
|
146
|
+
pp_table.add_column("Fix")
|
|
147
|
+
pp_table.add_column("Sessions", justify="right", style="dim")
|
|
148
|
+
pp_table.add_column("Avg turns", justify="right", style="dim")
|
|
149
|
+
pp_table.add_column("Waste", justify="right")
|
|
150
|
+
for pp in report.project_patterns:
|
|
151
|
+
pp_table.add_row(
|
|
152
|
+
pp.failure_key,
|
|
153
|
+
pp.fix_key,
|
|
154
|
+
str(pp.session_count),
|
|
155
|
+
f"{pp.avg_wasted_turns:.1f}",
|
|
156
|
+
f"~${pp.total_waste_usd:.2f}",
|
|
157
|
+
)
|
|
158
|
+
con.print(pp_table)
|
|
159
|
+
|
|
140
160
|
|
|
141
161
|
def render_aggregate_drilldown(
|
|
142
162
|
diagnoses: list,
|