cctx-cli 1.10.0__tar.gz → 1.11.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.10.0 → cctx_cli-1.11.0}/CHANGELOG.md +15 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/PKG-INFO +1 -1
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/cli.py +71 -2
- cctx_cli-1.11.0/cctx/hook_installer.py +101 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/pyproject.toml +1 -1
- cctx_cli-1.11.0/tests/test_init.py +323 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.gitignore +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/CLAUDE.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/DESIGN.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/PRODUCT.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/README.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/action.yml +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/agents.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/fan_out.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/harvest.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/models.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/recommender/claude_md.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/terminal.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/demo.gif +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/demo.tape +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/conftest.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_diagnostician_subagents.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_efficacy.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_fanout_classifier.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_harvest_emit.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_models.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.11.0 (2026-06-11)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Ruff E501 + I001 in test_init.py (line length + import order)
|
|
10
|
+
([#113](https://github.com/jacquardlabs/cctx/pull/113),
|
|
11
|
+
[`a04426e`](https://github.com/jacquardlabs/cctx/commit/a04426ef62cfeaa4ba7ea392bdf0dd49975cffbb))
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- Cctx init — SessionEnd hook installer + autopsy --quiet (closes #92)
|
|
16
|
+
([#113](https://github.com/jacquardlabs/cctx/pull/113),
|
|
17
|
+
[`a04426e`](https://github.com/jacquardlabs/cctx/commit/a04426ef62cfeaa4ba7ea392bdf0dd49975cffbb))
|
|
18
|
+
|
|
19
|
+
|
|
5
20
|
## v1.10.0 (2026-06-11)
|
|
6
21
|
|
|
7
22
|
### Features
|
|
@@ -9,6 +9,7 @@ Commands:
|
|
|
9
9
|
cctx harvest <session> Apply autopsy patches to CLAUDE.md
|
|
10
10
|
cctx harvest <project> --since Cross-session harvest
|
|
11
11
|
cctx watch [project] Live waste signals during an active session
|
|
12
|
+
cctx init Install opt-in SessionEnd hook for auto-diagnostics
|
|
12
13
|
"""
|
|
13
14
|
from __future__ import annotations
|
|
14
15
|
|
|
@@ -292,7 +293,15 @@ def ls(project: Path | None) -> None:
|
|
|
292
293
|
"json_out",
|
|
293
294
|
is_flag=True,
|
|
294
295
|
default=False,
|
|
295
|
-
help="Output diagnosis as JSON to stdout
|
|
296
|
+
help="Output diagnosis (or aggregate) as JSON to stdout.",
|
|
297
|
+
)
|
|
298
|
+
@click.option(
|
|
299
|
+
"--quiet",
|
|
300
|
+
"quiet",
|
|
301
|
+
is_flag=True,
|
|
302
|
+
default=False,
|
|
303
|
+
help="Print one verdict line only when findings exist; nothing if clean. "
|
|
304
|
+
"Designed for SessionEnd hook use (cctx init).",
|
|
296
305
|
)
|
|
297
306
|
def autopsy(
|
|
298
307
|
target: Path | None,
|
|
@@ -305,6 +314,7 @@ def autopsy(
|
|
|
305
314
|
top_n: int | None,
|
|
306
315
|
turn_num: int | None,
|
|
307
316
|
json_out: bool,
|
|
317
|
+
quiet: bool,
|
|
308
318
|
) -> None:
|
|
309
319
|
"""Diagnose a session or project directory.
|
|
310
320
|
|
|
@@ -408,7 +418,11 @@ def autopsy(
|
|
|
408
418
|
trace = tokenize_session(parse_session(target))
|
|
409
419
|
diagnosis = diagnostician.run(trace)
|
|
410
420
|
diagnosis = claude_md.generate(diagnosis)
|
|
411
|
-
if
|
|
421
|
+
if quiet:
|
|
422
|
+
if diagnosis.findings:
|
|
423
|
+
kinds = list(dict.fromkeys(f.kind.value for f in diagnosis.findings))
|
|
424
|
+
click.echo(f"{len(diagnosis.findings)} finding(s): {', '.join(kinds)}")
|
|
425
|
+
elif json_out:
|
|
412
426
|
import json as _json
|
|
413
427
|
|
|
414
428
|
from cctx.exporters.jsonl import export_diagnosis as _export_diag
|
|
@@ -729,3 +743,58 @@ def watch(target: Path | None) -> None:
|
|
|
729
743
|
"""
|
|
730
744
|
from cctx.watcher import watch as _watch
|
|
731
745
|
_watch(target)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@cli.command("init")
|
|
749
|
+
@click.option(
|
|
750
|
+
"--global",
|
|
751
|
+
"global_",
|
|
752
|
+
is_flag=True,
|
|
753
|
+
default=False,
|
|
754
|
+
help="Install to ~/.claude/settings.json (user scope) instead of .claude/settings.json.",
|
|
755
|
+
)
|
|
756
|
+
@click.option(
|
|
757
|
+
"--remove",
|
|
758
|
+
"remove_",
|
|
759
|
+
is_flag=True,
|
|
760
|
+
default=False,
|
|
761
|
+
help="Remove the SessionEnd hook instead of installing it.",
|
|
762
|
+
)
|
|
763
|
+
@click.option(
|
|
764
|
+
"--force",
|
|
765
|
+
is_flag=True,
|
|
766
|
+
default=False,
|
|
767
|
+
help="Reinstall even if hook is already present.",
|
|
768
|
+
)
|
|
769
|
+
def init_cmd(global_: bool, remove_: bool, force: bool) -> None:
|
|
770
|
+
"""Install an opt-in SessionEnd hook for automatic post-session diagnostics.
|
|
771
|
+
|
|
772
|
+
Writes a hook to .claude/settings.json (project) or ~/.claude/settings.json
|
|
773
|
+
(--global) that runs 'cctx autopsy --latest --quiet' when a Claude Code
|
|
774
|
+
session ends. Output appears only when findings exist.
|
|
775
|
+
|
|
776
|
+
Idempotent — running twice does not duplicate the hook.
|
|
777
|
+
"""
|
|
778
|
+
from cctx import hook_installer
|
|
779
|
+
|
|
780
|
+
if force and remove_:
|
|
781
|
+
raise click.UsageError("--force and --remove are mutually exclusive.")
|
|
782
|
+
|
|
783
|
+
scope = "~/.claude/settings.json" if global_ else ".claude/settings.json"
|
|
784
|
+
|
|
785
|
+
if remove_:
|
|
786
|
+
path = hook_installer.remove(global_=global_)
|
|
787
|
+
if path is None:
|
|
788
|
+
click.echo(f"No cctx hook found in {scope} — nothing to remove.")
|
|
789
|
+
else:
|
|
790
|
+
click.echo(f"✓ SessionEnd hook removed from {scope}")
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
result = hook_installer.install(global_=global_, force=force)
|
|
794
|
+
if result == "already_installed":
|
|
795
|
+
click.echo(f"! SessionEnd hook already installed in {scope}")
|
|
796
|
+
click.echo(" Use 'cctx init --force' to reinstall.")
|
|
797
|
+
else:
|
|
798
|
+
click.echo(f"✓ SessionEnd hook installed to {scope}")
|
|
799
|
+
remove_flag = "--global --remove" if global_ else "--remove"
|
|
800
|
+
click.echo(f" Run 'cctx init {remove_flag}' to uninstall.")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Settings-merge hook installer — install/remove the cctx SessionEnd hook.
|
|
2
|
+
|
|
3
|
+
Reads, merges, and writes ~/.claude/settings.json or .claude/settings.json
|
|
4
|
+
without touching any other keys. Idempotent: fingerprinted by the hook's
|
|
5
|
+
description field so a double-install is a no-op.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
HOOK_DESCRIPTION = "cctx SessionEnd hook (diagnostics on session exit)"
|
|
14
|
+
HOOK_COMMAND = "cctx autopsy --latest --quiet"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def settings_path(global_: bool) -> Path:
|
|
18
|
+
if global_:
|
|
19
|
+
return Path.home() / ".claude" / "settings.json"
|
|
20
|
+
return Path(".claude") / "settings.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load(path: Path) -> dict[str, Any]:
|
|
24
|
+
if not path.exists():
|
|
25
|
+
return {}
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
except json.JSONDecodeError as exc:
|
|
29
|
+
raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _save(path: Path, settings: dict[str, Any]) -> None:
|
|
33
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
tmp = path.with_suffix(".json.tmp")
|
|
35
|
+
tmp.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
|
|
36
|
+
tmp.replace(path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _find_hook(session_end: list[dict[str, Any]]) -> int | None:
|
|
40
|
+
"""Return the index of the cctx hook group in the SessionEnd array, or None."""
|
|
41
|
+
for i, group in enumerate(session_end):
|
|
42
|
+
for h in group.get("hooks", []):
|
|
43
|
+
desc = h.get("description", "")
|
|
44
|
+
if isinstance(desc, str) and "cctx SessionEnd" in desc:
|
|
45
|
+
return i
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _hook_entry() -> dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": HOOK_COMMAND,
|
|
55
|
+
"async": True,
|
|
56
|
+
"description": HOOK_DESCRIPTION,
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_installed(global_: bool = False) -> bool:
|
|
63
|
+
path = settings_path(global_)
|
|
64
|
+
settings = _load(path)
|
|
65
|
+
session_end = settings.get("hooks", {}).get("SessionEnd", [])
|
|
66
|
+
return _find_hook(session_end) is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def install(global_: bool = False, force: bool = False) -> str:
|
|
70
|
+
"""Install the hook. Returns "already_installed" or the path written."""
|
|
71
|
+
path = settings_path(global_)
|
|
72
|
+
settings = _load(path)
|
|
73
|
+
hooks = settings.setdefault("hooks", {})
|
|
74
|
+
session_end = hooks.setdefault("SessionEnd", [])
|
|
75
|
+
idx = _find_hook(session_end)
|
|
76
|
+
if idx is not None and not force:
|
|
77
|
+
return "already_installed"
|
|
78
|
+
if idx is not None:
|
|
79
|
+
session_end[idx] = _hook_entry()
|
|
80
|
+
else:
|
|
81
|
+
session_end.append(_hook_entry())
|
|
82
|
+
_save(path, settings)
|
|
83
|
+
return str(path)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def remove(global_: bool = False) -> str | None:
|
|
87
|
+
"""Remove the hook. Returns the path written, or None if not found."""
|
|
88
|
+
path = settings_path(global_)
|
|
89
|
+
settings = _load(path)
|
|
90
|
+
hooks = settings.get("hooks", {})
|
|
91
|
+
session_end = hooks.get("SessionEnd", [])
|
|
92
|
+
idx = _find_hook(session_end)
|
|
93
|
+
if idx is None:
|
|
94
|
+
return None
|
|
95
|
+
session_end.pop(idx)
|
|
96
|
+
if not session_end:
|
|
97
|
+
del hooks["SessionEnd"]
|
|
98
|
+
if not hooks:
|
|
99
|
+
del settings["hooks"]
|
|
100
|
+
_save(path, settings)
|
|
101
|
+
return str(path)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cctx-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.11.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"
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Tests for cctx init command and hook_installer module."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from click.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def runner():
|
|
13
|
+
return CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def settings_dir(tmp_path):
|
|
18
|
+
d = tmp_path / ".claude"
|
|
19
|
+
d.mkdir()
|
|
20
|
+
return d
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# hook_installer unit tests
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_install_creates_settings_file(tmp_path, monkeypatch):
|
|
29
|
+
"""install() writes .claude/settings.json with the hook entry."""
|
|
30
|
+
monkeypatch.chdir(tmp_path)
|
|
31
|
+
from cctx import hook_installer
|
|
32
|
+
|
|
33
|
+
result = hook_installer.install()
|
|
34
|
+
path = Path(".claude/settings.json")
|
|
35
|
+
assert path.exists()
|
|
36
|
+
data = json.loads(path.read_text())
|
|
37
|
+
session_end = data["hooks"]["SessionEnd"]
|
|
38
|
+
assert len(session_end) == 1
|
|
39
|
+
inner = session_end[0]["hooks"][0]
|
|
40
|
+
assert inner["type"] == "command"
|
|
41
|
+
assert "cctx autopsy --latest --quiet" in inner["command"]
|
|
42
|
+
assert "cctx SessionEnd" in inner["description"]
|
|
43
|
+
assert result == str(path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_install_idempotent(tmp_path, monkeypatch):
|
|
47
|
+
"""Running install() twice does not duplicate the hook."""
|
|
48
|
+
monkeypatch.chdir(tmp_path)
|
|
49
|
+
from cctx import hook_installer
|
|
50
|
+
|
|
51
|
+
hook_installer.install()
|
|
52
|
+
result = hook_installer.install()
|
|
53
|
+
assert result == "already_installed"
|
|
54
|
+
|
|
55
|
+
path = Path(".claude/settings.json")
|
|
56
|
+
data = json.loads(path.read_text())
|
|
57
|
+
assert len(data["hooks"]["SessionEnd"]) == 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_install_preserves_existing_settings(tmp_path, monkeypatch):
|
|
61
|
+
"""install() merges into existing settings without clobbering other keys."""
|
|
62
|
+
monkeypatch.chdir(tmp_path)
|
|
63
|
+
from cctx import hook_installer
|
|
64
|
+
|
|
65
|
+
settings_path = tmp_path / ".claude" / "settings.json"
|
|
66
|
+
settings_path.parent.mkdir()
|
|
67
|
+
existing = {
|
|
68
|
+
"permissions": {"allow": ["Bash(git*)"]},
|
|
69
|
+
"hooks": {
|
|
70
|
+
"PreToolUse": [
|
|
71
|
+
{"matcher": "Bash", "hooks": [{"type": "command", "command": "echo pre"}]}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
settings_path.write_text(json.dumps(existing))
|
|
76
|
+
|
|
77
|
+
hook_installer.install()
|
|
78
|
+
data = json.loads(settings_path.read_text())
|
|
79
|
+
|
|
80
|
+
# Existing keys preserved
|
|
81
|
+
assert data["permissions"]["allow"] == ["Bash(git*)"]
|
|
82
|
+
assert "PreToolUse" in data["hooks"]
|
|
83
|
+
# New hook added
|
|
84
|
+
assert "SessionEnd" in data["hooks"]
|
|
85
|
+
assert len(data["hooks"]["SessionEnd"]) == 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_install_force_replaces_hook(tmp_path, monkeypatch):
|
|
89
|
+
"""install(force=True) replaces existing hook without duplicating."""
|
|
90
|
+
monkeypatch.chdir(tmp_path)
|
|
91
|
+
from cctx import hook_installer
|
|
92
|
+
|
|
93
|
+
hook_installer.install()
|
|
94
|
+
result = hook_installer.install(force=True)
|
|
95
|
+
|
|
96
|
+
path = Path(".claude/settings.json")
|
|
97
|
+
data = json.loads(path.read_text())
|
|
98
|
+
assert len(data["hooks"]["SessionEnd"]) == 1
|
|
99
|
+
assert result != "already_installed"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_remove_cleans_up(tmp_path, monkeypatch):
|
|
103
|
+
"""remove() deletes the hook and prunes empty keys."""
|
|
104
|
+
monkeypatch.chdir(tmp_path)
|
|
105
|
+
from cctx import hook_installer
|
|
106
|
+
|
|
107
|
+
hook_installer.install()
|
|
108
|
+
result = hook_installer.remove()
|
|
109
|
+
|
|
110
|
+
path = Path(".claude/settings.json")
|
|
111
|
+
assert result is not None
|
|
112
|
+
data = json.loads(path.read_text())
|
|
113
|
+
assert "hooks" not in data
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_remove_preserves_other_hooks(tmp_path, monkeypatch):
|
|
117
|
+
"""remove() only removes the cctx hook, leaving other SessionEnd hooks."""
|
|
118
|
+
monkeypatch.chdir(tmp_path)
|
|
119
|
+
from cctx import hook_installer
|
|
120
|
+
|
|
121
|
+
settings_path = tmp_path / ".claude" / "settings.json"
|
|
122
|
+
settings_path.parent.mkdir()
|
|
123
|
+
other_hook = {"hooks": [{"type": "command", "command": "echo other"}]}
|
|
124
|
+
settings_path.write_text(json.dumps({
|
|
125
|
+
"hooks": {"SessionEnd": [other_hook]}
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
hook_installer.install()
|
|
129
|
+
hook_installer.remove()
|
|
130
|
+
|
|
131
|
+
data = json.loads(settings_path.read_text())
|
|
132
|
+
session_end = data["hooks"]["SessionEnd"]
|
|
133
|
+
assert len(session_end) == 1
|
|
134
|
+
assert session_end[0]["hooks"][0]["command"] == "echo other"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_remove_not_found_returns_none(tmp_path, monkeypatch):
|
|
138
|
+
"""remove() when hook is not installed returns None without error."""
|
|
139
|
+
monkeypatch.chdir(tmp_path)
|
|
140
|
+
from cctx import hook_installer
|
|
141
|
+
|
|
142
|
+
assert hook_installer.remove() is None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_is_installed_false_before_install(tmp_path, monkeypatch):
|
|
146
|
+
monkeypatch.chdir(tmp_path)
|
|
147
|
+
from cctx import hook_installer
|
|
148
|
+
|
|
149
|
+
assert not hook_installer.is_installed()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_is_installed_true_after_install(tmp_path, monkeypatch):
|
|
153
|
+
monkeypatch.chdir(tmp_path)
|
|
154
|
+
from cctx import hook_installer
|
|
155
|
+
|
|
156
|
+
hook_installer.install()
|
|
157
|
+
assert hook_installer.is_installed()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_invalid_json_raises(tmp_path, monkeypatch):
|
|
161
|
+
"""Corrupt settings.json → ValueError, not a silent overwrite."""
|
|
162
|
+
monkeypatch.chdir(tmp_path)
|
|
163
|
+
from cctx import hook_installer
|
|
164
|
+
|
|
165
|
+
path = tmp_path / ".claude" / "settings.json"
|
|
166
|
+
path.parent.mkdir()
|
|
167
|
+
path.write_text("{ not valid json }")
|
|
168
|
+
|
|
169
|
+
with pytest.raises(ValueError, match="Invalid JSON"):
|
|
170
|
+
hook_installer.install()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_global_scope_writes_to_home(tmp_path, monkeypatch):
|
|
174
|
+
"""--global writes to ~/.claude/settings.json."""
|
|
175
|
+
fake_home = tmp_path / "home"
|
|
176
|
+
fake_home.mkdir()
|
|
177
|
+
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
|
178
|
+
from cctx import hook_installer
|
|
179
|
+
|
|
180
|
+
hook_installer.install(global_=True)
|
|
181
|
+
path = fake_home / ".claude" / "settings.json"
|
|
182
|
+
assert path.exists()
|
|
183
|
+
data = json.loads(path.read_text())
|
|
184
|
+
assert "SessionEnd" in data["hooks"]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# CLI integration tests (cctx init)
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_init_installs_hook(runner, tmp_path):
|
|
193
|
+
"""cctx init writes the hook and prints confirmation."""
|
|
194
|
+
from cctx.cli import cli
|
|
195
|
+
|
|
196
|
+
result = runner.invoke(cli, ["init"], catch_exceptions=False, env={"PWD": str(tmp_path)})
|
|
197
|
+
# Note: monkeypatching cwd is simpler in unit tests; CLI uses Path(".claude/...")
|
|
198
|
+
# relative to cwd — just check exit code and output shape
|
|
199
|
+
assert result.exit_code == 0
|
|
200
|
+
assert (
|
|
201
|
+
"✓" in result.output
|
|
202
|
+
or "already" in result.output.lower()
|
|
203
|
+
or "installed" in result.output.lower()
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_init_idempotent_cli(runner, tmp_path, monkeypatch):
|
|
208
|
+
"""cctx init twice → second run reports already installed."""
|
|
209
|
+
monkeypatch.chdir(tmp_path)
|
|
210
|
+
from cctx.cli import cli
|
|
211
|
+
|
|
212
|
+
runner.invoke(cli, ["init"], catch_exceptions=False)
|
|
213
|
+
result = runner.invoke(cli, ["init"], catch_exceptions=False)
|
|
214
|
+
assert result.exit_code == 0
|
|
215
|
+
assert "already" in result.output.lower()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_init_remove_cli(runner, tmp_path, monkeypatch):
|
|
219
|
+
"""cctx init --remove cleans up the hook."""
|
|
220
|
+
monkeypatch.chdir(tmp_path)
|
|
221
|
+
from cctx.cli import cli
|
|
222
|
+
|
|
223
|
+
runner.invoke(cli, ["init"], catch_exceptions=False)
|
|
224
|
+
result = runner.invoke(cli, ["init", "--remove"], catch_exceptions=False)
|
|
225
|
+
assert result.exit_code == 0
|
|
226
|
+
assert "removed" in result.output.lower()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_init_remove_not_installed_cli(runner, tmp_path, monkeypatch):
|
|
230
|
+
"""cctx init --remove when not installed exits 0 with a friendly message."""
|
|
231
|
+
monkeypatch.chdir(tmp_path)
|
|
232
|
+
from cctx.cli import cli
|
|
233
|
+
|
|
234
|
+
result = runner.invoke(cli, ["init", "--remove"], catch_exceptions=False)
|
|
235
|
+
assert result.exit_code == 0
|
|
236
|
+
assert "nothing" in result.output.lower() or "not found" in result.output.lower()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_init_force_and_remove_errors(runner, tmp_path, monkeypatch):
|
|
240
|
+
"""--force --remove together → UsageError."""
|
|
241
|
+
monkeypatch.chdir(tmp_path)
|
|
242
|
+
from cctx.cli import cli
|
|
243
|
+
|
|
244
|
+
result = runner.invoke(cli, ["init", "--force", "--remove"])
|
|
245
|
+
assert result.exit_code != 0
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# autopsy --quiet tests
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _make_session_file(tmp_path: Path, session_id: str) -> Path:
|
|
254
|
+
line = {
|
|
255
|
+
"type": "user",
|
|
256
|
+
"uuid": f"{session_id}-u1",
|
|
257
|
+
"parentUuid": None,
|
|
258
|
+
"isSidechain": False,
|
|
259
|
+
"timestamp": "2026-05-14T10:00:00.000Z",
|
|
260
|
+
"sessionId": session_id,
|
|
261
|
+
"version": "2.1.138",
|
|
262
|
+
"cwd": "/Users/test/Projects/demo",
|
|
263
|
+
"gitBranch": "main",
|
|
264
|
+
"userType": "external",
|
|
265
|
+
"entrypoint": "cli",
|
|
266
|
+
"message": {"role": "user", "content": "hello"},
|
|
267
|
+
}
|
|
268
|
+
path = tmp_path / f"{session_id}.jsonl"
|
|
269
|
+
path.write_text(json.dumps(line) + "\n")
|
|
270
|
+
return path
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_autopsy_quiet_clean_no_output(runner, tmp_path):
|
|
274
|
+
"""--quiet on a clean session emits nothing and exits 0."""
|
|
275
|
+
from cctx.cli import cli
|
|
276
|
+
|
|
277
|
+
session = _make_session_file(tmp_path, "quiet-clean-01")
|
|
278
|
+
result = runner.invoke(cli, ["autopsy", str(session), "--quiet"], catch_exceptions=False)
|
|
279
|
+
assert result.exit_code == 0
|
|
280
|
+
assert result.output.strip() == ""
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_autopsy_quiet_with_findings_emits_verdict(runner, tmp_path, monkeypatch):
|
|
284
|
+
"""--quiet on a session with findings emits one verdict line."""
|
|
285
|
+
from cctx.cli import cli
|
|
286
|
+
from cctx.models import Confidence, Finding, FindingKind, Severity
|
|
287
|
+
|
|
288
|
+
session = _make_session_file(tmp_path, "quiet-dirty-01")
|
|
289
|
+
|
|
290
|
+
# Inject a finding via monkeypatch so we don't need a real problematic session
|
|
291
|
+
from datetime import datetime, timezone
|
|
292
|
+
|
|
293
|
+
from cctx import diagnostician
|
|
294
|
+
from cctx.models import Diagnosis
|
|
295
|
+
|
|
296
|
+
def fake_run(trace):
|
|
297
|
+
return Diagnosis(
|
|
298
|
+
session_id=trace.session_id,
|
|
299
|
+
findings=[
|
|
300
|
+
Finding(
|
|
301
|
+
kind=FindingKind.RETRY_LOOP,
|
|
302
|
+
severity=Severity.HIGH,
|
|
303
|
+
confidence=Confidence.HIGH,
|
|
304
|
+
first_turn=1,
|
|
305
|
+
last_turn=2,
|
|
306
|
+
evidence={},
|
|
307
|
+
cost_usd=0.01,
|
|
308
|
+
summary="test finding",
|
|
309
|
+
)
|
|
310
|
+
],
|
|
311
|
+
inflection_turn=1,
|
|
312
|
+
patches=[],
|
|
313
|
+
total_cost_usd=0.10,
|
|
314
|
+
waste_cost_usd=0.01,
|
|
315
|
+
analysed_at=datetime(2026, 5, 14, 10, 0, tzinfo=timezone.utc),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
monkeypatch.setattr(diagnostician, "run", fake_run)
|
|
319
|
+
|
|
320
|
+
result = runner.invoke(cli, ["autopsy", str(session), "--quiet"], catch_exceptions=False)
|
|
321
|
+
assert result.exit_code == 0
|
|
322
|
+
assert "finding" in result.output.lower()
|
|
323
|
+
assert "retry_loop" in result.output.lower()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|