cctx-cli 1.3.0__tar.gz → 1.5.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.3.0 → cctx_cli-1.5.0}/CHANGELOG.md +101 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/PKG-INFO +1 -1
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/__init__.py +1 -1
- cctx_cli-1.5.0/cctx/agents.py +66 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/cli.py +42 -9
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/harvest.py +187 -18
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/terminal.py +35 -4
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/watcher.py +37 -11
- cctx_cli-1.5.0/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +860 -0
- cctx_cli-1.5.0/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +839 -0
- cctx_cli-1.5.0/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +214 -0
- cctx_cli-1.5.0/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +128 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/pyproject.toml +1 -1
- cctx_cli-1.5.0/tests/test_agents.py +128 -0
- cctx_cli-1.5.0/tests/test_harvest_check.py +418 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_terminal_renderer.py +154 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_watcher.py +101 -0
- cctx_cli-1.3.0/tests/test_harvest_check.py +0 -139
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.gitignore +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/CLAUDE.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/DESIGN.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/PRODUCT.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/README.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/action.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/models.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/recommender/claude_md.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/demo.gif +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/demo.tape +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/conftest.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_models.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_trace_tui.py +0 -0
|
@@ -2,6 +2,107 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.5.0 (2026-05-20)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Agents.py — guard against non-list JSON, tighten patch targets
|
|
10
|
+
([`c41c42c`](https://github.com/jacquardlabs/cctx/commit/c41c42cb366904fc332638b8f97ee042f17b45c2))
|
|
11
|
+
|
|
12
|
+
- Renderer — guard order, tmp_path fixtures, missing no-badge tests
|
|
13
|
+
([`8f26afd`](https://github.com/jacquardlabs/cctx/commit/8f26afdf27c63641942b434e47a2fb7b24d38068))
|
|
14
|
+
|
|
15
|
+
- Watcher — hermetic tests, reuse _encode_path, rename clarity
|
|
16
|
+
([`a7b52de`](https://github.com/jacquardlabs/cctx/commit/a7b52def47b13447d956476a0ec6782fe2d0247b))
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
- Implementation plan for claude agents live integration
|
|
21
|
+
([`2136adf`](https://github.com/jacquardlabs/cctx/commit/2136adf0697a695d27f438f0368e7bd5ba406e89))
|
|
22
|
+
|
|
23
|
+
- Spec for claude agents --json live session integration
|
|
24
|
+
([`0df2381`](https://github.com/jacquardlabs/cctx/commit/0df23813a90c66e57d0d39c6b959859b89a5c057))
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
- Add agents.py — live_sessions() via claude agents --json
|
|
29
|
+
([`83b704f`](https://github.com/jacquardlabs/cctx/commit/83b704ffbe4303dbd316257a01eb0b59307c0e06))
|
|
30
|
+
|
|
31
|
+
- Cctx ls — pass live_statuses to renderer for live session badges
|
|
32
|
+
([`65445d7`](https://github.com/jacquardlabs/cctx/commit/65445d75a213bf26401fe044a2409d2ce0efbcb1))
|
|
33
|
+
|
|
34
|
+
- Render_sessions/render_projects — live status badges via live_statuses param
|
|
35
|
+
([`3d77687`](https://github.com/jacquardlabs/cctx/commit/3d776879c9ce03a622361c2b804e5986d40f37a1))
|
|
36
|
+
|
|
37
|
+
- Watcher — live session detection + early idle exit via claude agents --json
|
|
38
|
+
([`a11481c`](https://github.com/jacquardlabs/cctx/commit/a11481c2126fead87e1f92bfecc7bf5ac3f39d1a))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## v1.4.0 (2026-05-20)
|
|
42
|
+
|
|
43
|
+
### Bug Fixes
|
|
44
|
+
|
|
45
|
+
- Deduplicate harvest check import, align severity badge output
|
|
46
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
47
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
48
|
+
|
|
49
|
+
- Move defaultdict import to top-level, add _words() return type
|
|
50
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
51
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
52
|
+
|
|
53
|
+
- Use removeprefix instead of lstrip to preserve .claude/skills/ dot prefix
|
|
54
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
55
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
56
|
+
|
|
57
|
+
### Documentation
|
|
58
|
+
|
|
59
|
+
- M15 harvest --check depth design spec ([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
60
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
61
|
+
|
|
62
|
+
- M15 harvest --check depth implementation plan
|
|
63
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
64
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
65
|
+
|
|
66
|
+
### Features
|
|
67
|
+
|
|
68
|
+
- --check-severity flag and severity badges in harvest --check output
|
|
69
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
70
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
71
|
+
|
|
72
|
+
- Check_contradictions() — always/never keyword heuristic
|
|
73
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
74
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
75
|
+
|
|
76
|
+
- Check_redundancy() — Jaccard similarity ≥ 0.8 on section word sets
|
|
77
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
78
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
79
|
+
|
|
80
|
+
- Check_staleness() — backtick function refs grepped against project source
|
|
81
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
82
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
83
|
+
|
|
84
|
+
- CheckSeverity enum, severity field on CheckFinding, new CheckIssue values
|
|
85
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
86
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
87
|
+
|
|
88
|
+
- Harvest --check depth — contradiction, redundancy, staleness detectors + --check-severity
|
|
89
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
90
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
91
|
+
|
|
92
|
+
- Wire all four detectors into check_claude_md ([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
93
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
94
|
+
|
|
95
|
+
### Refactoring
|
|
96
|
+
|
|
97
|
+
- Check_redundancy — compute _words once per section, remove dead union guard
|
|
98
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
99
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
100
|
+
|
|
101
|
+
- Check_staleness — module-level _STALENESS_EXCLUDED, min-len in regex, per-file search
|
|
102
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
103
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
104
|
+
|
|
105
|
+
|
|
5
106
|
## v1.3.0 (2026-05-17)
|
|
6
107
|
|
|
7
108
|
### Bug Fixes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Live Claude Code agent query via `claude agents --json`.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
live_sessions() -> list[LiveSession]
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class LiveSession:
|
|
16
|
+
session_id: str # matches JSONL filename stem in ~/.claude/projects/
|
|
17
|
+
cwd: str
|
|
18
|
+
status: str # "busy" | "idle"
|
|
19
|
+
pid: int
|
|
20
|
+
kind: str # "interactive" | "background"
|
|
21
|
+
started_at: datetime
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def live_sessions() -> list[LiveSession]:
|
|
25
|
+
"""Query `claude agents --json`. Returns [] on any failure."""
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["claude", "agents", "--json"],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=2,
|
|
32
|
+
)
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return []
|
|
35
|
+
except subprocess.TimeoutExpired:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
if result.returncode != 0:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
data = json.loads(result.stdout)
|
|
43
|
+
except json.JSONDecodeError:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
if not isinstance(data, list):
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
sessions: list[LiveSession] = []
|
|
50
|
+
for item in data:
|
|
51
|
+
try:
|
|
52
|
+
sessions.append(
|
|
53
|
+
LiveSession(
|
|
54
|
+
session_id=item["sessionId"],
|
|
55
|
+
cwd=item["cwd"],
|
|
56
|
+
status=item.get("status", "unknown"),
|
|
57
|
+
pid=int(item["pid"]),
|
|
58
|
+
kind=item.get("kind", "interactive"),
|
|
59
|
+
started_at=datetime.fromtimestamp(
|
|
60
|
+
item["startedAt"] / 1000, tz=timezone.utc
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
except (KeyError, TypeError, ValueError):
|
|
65
|
+
continue
|
|
66
|
+
return sessions
|
|
@@ -19,6 +19,7 @@ from typing import IO
|
|
|
19
19
|
import rich_click as click
|
|
20
20
|
|
|
21
21
|
from cctx import diagnostician
|
|
22
|
+
from cctx.agents import live_sessions as _live_sessions
|
|
22
23
|
from cctx.diagnostician import aggregate
|
|
23
24
|
from cctx.diagnostician.patterns import project_specific
|
|
24
25
|
from cctx.discovery import complete_project as _complete_project
|
|
@@ -147,22 +148,32 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
|
|
|
147
148
|
from rich.console import Console
|
|
148
149
|
from rich.rule import Rule
|
|
149
150
|
|
|
151
|
+
from cctx.harvest import CheckIssue, CheckSeverity
|
|
152
|
+
|
|
150
153
|
con = Console()
|
|
151
154
|
claude_md_path = target_dir / "CLAUDE.md"
|
|
152
155
|
con.print(Rule(f"cctx harvest --check — {claude_md_path}"))
|
|
153
156
|
if not findings:
|
|
154
|
-
con.print("✓ CLAUDE.md looks clean — no
|
|
157
|
+
con.print("✓ CLAUDE.md looks clean — no issues found.")
|
|
155
158
|
return
|
|
156
159
|
con.print(f"{len(findings)} issue(s) found:\n")
|
|
157
|
-
from cctx.harvest import CheckIssue
|
|
158
160
|
_ISSUE_LABEL = {
|
|
159
|
-
CheckIssue.DEAD_FILE_REF:
|
|
160
|
-
CheckIssue.DEAD_SKILL_REF:
|
|
161
|
-
CheckIssue.EMPTY_SECTION:
|
|
161
|
+
CheckIssue.DEAD_FILE_REF: "dead file reference",
|
|
162
|
+
CheckIssue.DEAD_SKILL_REF: "dead skill reference",
|
|
163
|
+
CheckIssue.EMPTY_SECTION: "empty section",
|
|
164
|
+
CheckIssue.CONTRADICTION: "contradiction",
|
|
165
|
+
CheckIssue.REDUNDANCY: "redundancy",
|
|
166
|
+
CheckIssue.STALE_IDENTIFIER: "stale identifier",
|
|
167
|
+
}
|
|
168
|
+
_SEV_BADGE = {
|
|
169
|
+
CheckSeverity.HIGH: "[HIGH]",
|
|
170
|
+
CheckSeverity.MEDIUM: "[MED]",
|
|
171
|
+
CheckSeverity.LOW: "[LOW]",
|
|
162
172
|
}
|
|
163
173
|
for f in findings:
|
|
174
|
+
badge = _SEV_BADGE.get(f.severity, " ")
|
|
164
175
|
label = _ISSUE_LABEL.get(f.issue, f.issue.value)
|
|
165
|
-
con.print(f"
|
|
176
|
+
con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
|
|
166
177
|
|
|
167
178
|
|
|
168
179
|
@click.group()
|
|
@@ -186,9 +197,11 @@ def ls(project: Path | None) -> None:
|
|
|
186
197
|
"""
|
|
187
198
|
from cctx.discovery import ProjectInfo, find_project_dir, list_projects, list_sessions
|
|
188
199
|
|
|
200
|
+
live_statuses = {s.session_id: s.status for s in _live_sessions()}
|
|
201
|
+
|
|
189
202
|
if project is None:
|
|
190
203
|
projects = list_projects()
|
|
191
|
-
render_projects(projects)
|
|
204
|
+
render_projects(projects, live_statuses=live_statuses)
|
|
192
205
|
else:
|
|
193
206
|
cwd = project if project.is_dir() else project.parent
|
|
194
207
|
project_dir = find_project_dir(cwd)
|
|
@@ -203,7 +216,7 @@ def ls(project: Path | None) -> None:
|
|
|
203
216
|
display_name=str(cwd).replace(str(Path.home()), "~"),
|
|
204
217
|
sessions=sessions,
|
|
205
218
|
)
|
|
206
|
-
render_sessions(info)
|
|
219
|
+
render_sessions(info, live_statuses=live_statuses)
|
|
207
220
|
|
|
208
221
|
|
|
209
222
|
@cli.command()
|
|
@@ -549,6 +562,14 @@ def trace(target: Path | None, latest: bool) -> None:
|
|
|
549
562
|
default=False,
|
|
550
563
|
help="Audit existing CLAUDE.md for dead references and empty sections. Exit 1 if findings.",
|
|
551
564
|
)
|
|
565
|
+
@click.option(
|
|
566
|
+
"--check-severity",
|
|
567
|
+
"check_severity",
|
|
568
|
+
default="MEDIUM",
|
|
569
|
+
type=click.Choice(["LOW", "MEDIUM", "HIGH"], case_sensitive=False),
|
|
570
|
+
show_default=True,
|
|
571
|
+
help="Minimum severity that causes --check to exit 1.",
|
|
572
|
+
)
|
|
552
573
|
def harvest(
|
|
553
574
|
target: Path,
|
|
554
575
|
since: str | None,
|
|
@@ -556,15 +577,27 @@ def harvest(
|
|
|
556
577
|
dry_run: bool,
|
|
557
578
|
target_dir: Path | None,
|
|
558
579
|
check_mode: bool,
|
|
580
|
+
check_severity: str,
|
|
559
581
|
) -> None:
|
|
560
582
|
"""Apply autopsy patches to CLAUDE.md."""
|
|
561
583
|
from cctx.harvest import apply_patches, check_claude_md, preview_patches
|
|
562
584
|
|
|
563
585
|
if check_mode:
|
|
586
|
+
from cctx.harvest import CheckSeverity
|
|
564
587
|
resolved_dir = target_dir or Path.cwd()
|
|
565
588
|
findings = check_claude_md(resolved_dir)
|
|
566
589
|
_render_check_findings(findings, resolved_dir)
|
|
567
|
-
|
|
590
|
+
_SEVERITY_ORDER = {
|
|
591
|
+
CheckSeverity.LOW: 0,
|
|
592
|
+
CheckSeverity.MEDIUM: 1,
|
|
593
|
+
CheckSeverity.HIGH: 2,
|
|
594
|
+
}
|
|
595
|
+
threshold = CheckSeverity(check_severity.lower())
|
|
596
|
+
triggering = [
|
|
597
|
+
f for f in findings
|
|
598
|
+
if _SEVERITY_ORDER[f.severity] >= _SEVERITY_ORDER[threshold]
|
|
599
|
+
]
|
|
600
|
+
raise SystemExit(1 if triggering else 0)
|
|
568
601
|
|
|
569
602
|
if apply_mode and dry_run:
|
|
570
603
|
raise click.UsageError("--apply and --dry-run are mutually exclusive.")
|
|
@@ -14,6 +14,7 @@ Layering rules (MUST respect):
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import re
|
|
17
|
+
from collections import defaultdict
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from enum import Enum
|
|
19
20
|
from pathlib import Path
|
|
@@ -28,17 +29,27 @@ if TYPE_CHECKING:
|
|
|
28
29
|
# ---------------------------------------------------------------------------
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
class CheckSeverity(str, Enum):
|
|
33
|
+
LOW = "low"
|
|
34
|
+
MEDIUM = "medium"
|
|
35
|
+
HIGH = "high"
|
|
36
|
+
|
|
37
|
+
|
|
31
38
|
class CheckIssue(str, Enum):
|
|
32
|
-
DEAD_FILE_REF
|
|
33
|
-
DEAD_SKILL_REF
|
|
34
|
-
EMPTY_SECTION
|
|
39
|
+
DEAD_FILE_REF = "dead_file_ref"
|
|
40
|
+
DEAD_SKILL_REF = "dead_skill_ref"
|
|
41
|
+
EMPTY_SECTION = "empty_section"
|
|
42
|
+
CONTRADICTION = "contradiction"
|
|
43
|
+
REDUNDANCY = "redundancy"
|
|
44
|
+
STALE_IDENTIFIER = "stale_identifier"
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
@dataclass
|
|
38
48
|
class CheckFinding:
|
|
39
|
-
heading:
|
|
40
|
-
issue:
|
|
41
|
-
|
|
49
|
+
heading: str
|
|
50
|
+
issue: CheckIssue
|
|
51
|
+
severity: CheckSeverity
|
|
52
|
+
detail: str
|
|
42
53
|
|
|
43
54
|
|
|
44
55
|
class ApplyStatus(str, Enum):
|
|
@@ -212,6 +223,25 @@ _KNOWN_EXTENSIONS = {
|
|
|
212
223
|
".json", ".md", ".sh", ".bash", ".fish", ".zsh",
|
|
213
224
|
}
|
|
214
225
|
|
|
226
|
+
_STOPWORDS = {
|
|
227
|
+
"a", "an", "the", "to", "be", "is", "are", "was", "were",
|
|
228
|
+
"in", "on", "at", "of", "for", "with", "and", "or", "not",
|
|
229
|
+
"it", "this", "that", "you", "your", "use", "do",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_ALWAYS_NEVER_RE = re.compile(
|
|
233
|
+
r"\b(always|never)\b(.+?)(?:[.!?\n]|$)", re.IGNORECASE
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
_STALENESS_EXCLUDED = {".git", ".venv", "node_modules", "__pycache__"}
|
|
237
|
+
|
|
238
|
+
_FUNC_REF_RE = re.compile(r"`([^`/.\s]{8,})\(\)`")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _words(text: str) -> set[str]:
|
|
242
|
+
tokens = re.findall(r"\b[a-zA-Z_]\w*\b", text.lower())
|
|
243
|
+
return {t for t in tokens if t not in _STOPWORDS}
|
|
244
|
+
|
|
215
245
|
|
|
216
246
|
def _parse_sections(content: str) -> list[tuple[str, str]]:
|
|
217
247
|
"""Split markdown into (heading, body) pairs.
|
|
@@ -233,22 +263,133 @@ def _parse_sections(content: str) -> list[tuple[str, str]]:
|
|
|
233
263
|
return sections
|
|
234
264
|
|
|
235
265
|
|
|
236
|
-
def
|
|
237
|
-
|
|
266
|
+
def check_contradictions(
|
|
267
|
+
sections: list[tuple[str, str]],
|
|
268
|
+
) -> list[CheckFinding]:
|
|
269
|
+
"""Detect contradictions across sections using always/never polarity heuristic.
|
|
238
270
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- Dead skill references: .claude/skills/ paths that don't exist
|
|
242
|
-
- Empty sections: ## headings with no content
|
|
271
|
+
Looks for "always" and "never" clauses in section bodies, extracts the
|
|
272
|
+
subject words, and flags cases where the same word has conflicting polarities.
|
|
243
273
|
|
|
244
|
-
Returns
|
|
274
|
+
Returns findings for each contradiction found (severity: HIGH).
|
|
245
275
|
"""
|
|
246
|
-
|
|
247
|
-
|
|
276
|
+
subject_map: dict[str, list[tuple[str, str]]] = defaultdict(list)
|
|
277
|
+
for heading, body in sections:
|
|
278
|
+
for match in _ALWAYS_NEVER_RE.finditer(body):
|
|
279
|
+
polarity = match.group(1).lower()
|
|
280
|
+
clause = match.group(2)
|
|
281
|
+
for word in _words(clause):
|
|
282
|
+
subject_map[word].append((polarity, heading))
|
|
283
|
+
|
|
284
|
+
findings: list[CheckFinding] = []
|
|
285
|
+
seen: set[tuple[str, str]] = set()
|
|
286
|
+
for word, occurrences in subject_map.items():
|
|
287
|
+
always_headings = [h for p, h in occurrences if p == "always"]
|
|
288
|
+
never_headings = [h for p, h in occurrences if p == "never"]
|
|
289
|
+
if always_headings and never_headings:
|
|
290
|
+
key = (always_headings[0], never_headings[0])
|
|
291
|
+
if key not in seen:
|
|
292
|
+
seen.add(key)
|
|
293
|
+
findings.append(CheckFinding(
|
|
294
|
+
heading=always_headings[0],
|
|
295
|
+
issue=CheckIssue.CONTRADICTION,
|
|
296
|
+
severity=CheckSeverity.HIGH,
|
|
297
|
+
detail=(
|
|
298
|
+
f"'{word}' is 'always' in {always_headings[0]!r}"
|
|
299
|
+
f" but 'never' in {never_headings[0]!r}"
|
|
300
|
+
),
|
|
301
|
+
))
|
|
302
|
+
return findings
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def check_redundancy(
|
|
306
|
+
sections: list[tuple[str, str]],
|
|
307
|
+
) -> list[CheckFinding]:
|
|
308
|
+
"""Detect redundancy across sections using Jaccard similarity.
|
|
309
|
+
|
|
310
|
+
Builds a word set (stopwords removed) for each section. Sections with
|
|
311
|
+
fewer than 5 words are ineligible. For all pairs of eligible sections,
|
|
312
|
+
computes Jaccard similarity of their word sets. Flags pairs with
|
|
313
|
+
similarity >= 0.8.
|
|
314
|
+
|
|
315
|
+
Returns findings for each redundancy found (severity: MEDIUM).
|
|
316
|
+
"""
|
|
317
|
+
eligible = []
|
|
318
|
+
for heading, body in sections:
|
|
319
|
+
ws = _words(body)
|
|
320
|
+
if len(ws) >= 5:
|
|
321
|
+
eligible.append((heading, body, ws))
|
|
322
|
+
|
|
323
|
+
findings: list[CheckFinding] = []
|
|
324
|
+
for i in range(len(eligible)):
|
|
325
|
+
for j in range(i + 1, len(eligible)):
|
|
326
|
+
h1, _, w1 = eligible[i]
|
|
327
|
+
h2, _, w2 = eligible[j]
|
|
328
|
+
union = w1 | w2
|
|
329
|
+
jaccard = len(w1 & w2) / len(union)
|
|
330
|
+
if jaccard >= 0.8:
|
|
331
|
+
findings.append(CheckFinding(
|
|
332
|
+
heading=h1,
|
|
333
|
+
issue=CheckIssue.REDUNDANCY,
|
|
334
|
+
severity=CheckSeverity.MEDIUM,
|
|
335
|
+
detail=f"{h1!r} and {h2!r} are {jaccard:.0%} similar",
|
|
336
|
+
))
|
|
337
|
+
return findings
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def check_staleness(
|
|
341
|
+
sections: list[tuple[str, str]],
|
|
342
|
+
project_dir: Path,
|
|
343
|
+
) -> list[CheckFinding]:
|
|
344
|
+
"""Detect stale function references in CLAUDE.md.
|
|
345
|
+
|
|
346
|
+
Scans all .py, .ts, and .js source files in the project directory and
|
|
347
|
+
searches for backtick-quoted function references (e.g., `my_function()`)
|
|
348
|
+
that are 8+ characters long. Flags references not found in the source.
|
|
349
|
+
|
|
350
|
+
Returns findings for each stale identifier found (severity: LOW).
|
|
351
|
+
"""
|
|
352
|
+
source_files = [
|
|
353
|
+
f
|
|
354
|
+
for f in (
|
|
355
|
+
list(project_dir.rglob("*.py"))
|
|
356
|
+
+ list(project_dir.rglob("*.ts"))
|
|
357
|
+
+ list(project_dir.rglob("*.js"))
|
|
358
|
+
)
|
|
359
|
+
if not any(part in _STALENESS_EXCLUDED for part in f.parts)
|
|
360
|
+
]
|
|
361
|
+
if not source_files:
|
|
248
362
|
return []
|
|
249
363
|
|
|
250
|
-
|
|
251
|
-
|
|
364
|
+
findings: list[CheckFinding] = []
|
|
365
|
+
for heading, body in sections:
|
|
366
|
+
for match in _FUNC_REF_RE.finditer(body):
|
|
367
|
+
name = match.group(1)
|
|
368
|
+
found = any(
|
|
369
|
+
name in f.read_text(encoding="utf-8", errors="ignore")
|
|
370
|
+
for f in source_files
|
|
371
|
+
)
|
|
372
|
+
if not found:
|
|
373
|
+
findings.append(CheckFinding(
|
|
374
|
+
heading=heading,
|
|
375
|
+
issue=CheckIssue.STALE_IDENTIFIER,
|
|
376
|
+
severity=CheckSeverity.LOW,
|
|
377
|
+
detail=f"'{name}()' not found in project source files",
|
|
378
|
+
))
|
|
379
|
+
return findings
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _check_structure(
|
|
383
|
+
sections: list[tuple[str, str]],
|
|
384
|
+
target_dir: Path,
|
|
385
|
+
) -> list[CheckFinding]:
|
|
386
|
+
"""Check structure issues: empty sections, dead file/skill references.
|
|
387
|
+
|
|
388
|
+
Returns findings for:
|
|
389
|
+
- Empty sections: ## headings with no content (MEDIUM)
|
|
390
|
+
- Dead file references: backtick-quoted paths that don't exist (MEDIUM)
|
|
391
|
+
- Dead skill references: .claude/skills/ paths that don't exist (MEDIUM)
|
|
392
|
+
"""
|
|
252
393
|
findings: list[CheckFinding] = []
|
|
253
394
|
|
|
254
395
|
for heading, body in sections:
|
|
@@ -259,13 +400,14 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
|
259
400
|
findings.append(CheckFinding(
|
|
260
401
|
heading=heading,
|
|
261
402
|
issue=CheckIssue.EMPTY_SECTION,
|
|
403
|
+
severity=CheckSeverity.MEDIUM,
|
|
262
404
|
detail=f"{heading!r} has no content",
|
|
263
405
|
))
|
|
264
406
|
continue
|
|
265
407
|
|
|
266
408
|
# Dead skill references
|
|
267
409
|
for match in _SKILL_REF_RE.finditer(body):
|
|
268
|
-
skill_path_str = match.group(1).
|
|
410
|
+
skill_path_str = match.group(1).removeprefix("./")
|
|
269
411
|
# Try resolving from target_dir and from home
|
|
270
412
|
candidates = [
|
|
271
413
|
target_dir / skill_path_str,
|
|
@@ -275,6 +417,7 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
|
275
417
|
findings.append(CheckFinding(
|
|
276
418
|
heading=heading,
|
|
277
419
|
issue=CheckIssue.DEAD_SKILL_REF,
|
|
420
|
+
severity=CheckSeverity.MEDIUM,
|
|
278
421
|
detail=f"skill not found: {match.group(1)!r}",
|
|
279
422
|
))
|
|
280
423
|
|
|
@@ -292,7 +435,33 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
|
292
435
|
findings.append(CheckFinding(
|
|
293
436
|
heading=heading,
|
|
294
437
|
issue=CheckIssue.DEAD_FILE_REF,
|
|
438
|
+
severity=CheckSeverity.MEDIUM,
|
|
295
439
|
detail=f"file not found: {token!r}",
|
|
296
440
|
))
|
|
297
441
|
|
|
298
442
|
return findings
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
446
|
+
"""Audit CLAUDE.md in target_dir for deterministically detectable issues.
|
|
447
|
+
|
|
448
|
+
Checks:
|
|
449
|
+
- Dead file/skill references and empty sections (MEDIUM)
|
|
450
|
+
- Contradictory always/never rules (HIGH)
|
|
451
|
+
- Redundant sections with Jaccard >= 0.8 (MEDIUM)
|
|
452
|
+
- Stale backtick-quoted function identifiers >= 8 chars (LOW)
|
|
453
|
+
|
|
454
|
+
Returns an empty list if CLAUDE.md doesn't exist (not an error).
|
|
455
|
+
"""
|
|
456
|
+
claude_md = target_dir / "CLAUDE.md"
|
|
457
|
+
if not claude_md.exists():
|
|
458
|
+
return []
|
|
459
|
+
|
|
460
|
+
content = claude_md.read_text(encoding="utf-8")
|
|
461
|
+
sections = _parse_sections(content)
|
|
462
|
+
return (
|
|
463
|
+
_check_structure(sections, target_dir)
|
|
464
|
+
+ check_contradictions(sections)
|
|
465
|
+
+ check_redundancy(sections)
|
|
466
|
+
+ check_staleness(sections, target_dir)
|
|
467
|
+
)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
render_diagnosis(diagnosis, console=None) -> None
|
|
4
4
|
render_aggregate(report, console=None) -> None
|
|
5
5
|
render_harvest_results(results, dry_run=False, console=None) -> None
|
|
6
|
-
render_projects(projects, console=None) -> None
|
|
7
|
-
render_sessions(project, console=None) -> None
|
|
6
|
+
render_projects(projects, live_statuses=None, console=None) -> None
|
|
7
|
+
render_sessions(project, live_statuses=None, console=None) -> None
|
|
8
8
|
|
|
9
9
|
Uses rich for formatting. Accepts an optional Console for testing.
|
|
10
10
|
"""
|
|
@@ -294,25 +294,44 @@ def render_harvest_results(
|
|
|
294
294
|
con.print(f"Applied {applied_count} patch(es).")
|
|
295
295
|
|
|
296
296
|
|
|
297
|
-
def render_projects(
|
|
297
|
+
def render_projects(
|
|
298
|
+
projects: list[ProjectInfo],
|
|
299
|
+
*,
|
|
300
|
+
live_statuses: dict[str, str] | None = None,
|
|
301
|
+
console: Console | None = None,
|
|
302
|
+
) -> None:
|
|
298
303
|
con = console or _default_console()
|
|
299
304
|
|
|
300
305
|
if not projects:
|
|
301
306
|
con.print("No projects found in ~/.claude/projects/.")
|
|
302
307
|
return
|
|
303
308
|
|
|
309
|
+
_live = live_statuses or {}
|
|
310
|
+
live_project_ids: set[str] = {
|
|
311
|
+
proj.project_dir.name
|
|
312
|
+
for proj in projects
|
|
313
|
+
for s in proj.sessions
|
|
314
|
+
if s.session_id in _live
|
|
315
|
+
}
|
|
316
|
+
|
|
304
317
|
con.print(Rule("cctx — projects"))
|
|
305
318
|
table = Table(show_header=True, box=None, pad_edge=False, show_edge=False)
|
|
306
319
|
table.add_column("Project", style="bold")
|
|
307
320
|
table.add_column("Sessions", justify="right", style="dim")
|
|
308
321
|
table.add_column("Last session", style="dim")
|
|
322
|
+
table.add_column("Status")
|
|
309
323
|
|
|
310
324
|
for proj in projects:
|
|
311
325
|
last = proj.latest_time.strftime("%Y-%m-%d") if proj.latest_time else "—"
|
|
326
|
+
if proj.project_dir.name in live_project_ids:
|
|
327
|
+
status_cell = Text("● live", style="green bold")
|
|
328
|
+
else:
|
|
329
|
+
status_cell = Text("")
|
|
312
330
|
table.add_row(
|
|
313
331
|
proj.display_name,
|
|
314
332
|
str(proj.session_count),
|
|
315
333
|
last,
|
|
334
|
+
status_cell,
|
|
316
335
|
)
|
|
317
336
|
con.print(table)
|
|
318
337
|
con.print()
|
|
@@ -326,8 +345,14 @@ def render_projects(projects: list[ProjectInfo], *, console: Console | None = No
|
|
|
326
345
|
)
|
|
327
346
|
|
|
328
347
|
|
|
329
|
-
def render_sessions(
|
|
348
|
+
def render_sessions(
|
|
349
|
+
project: ProjectInfo,
|
|
350
|
+
*,
|
|
351
|
+
live_statuses: dict[str, str] | None = None,
|
|
352
|
+
console: Console | None = None,
|
|
353
|
+
) -> None:
|
|
330
354
|
con = console or _default_console()
|
|
355
|
+
_live = live_statuses or {}
|
|
331
356
|
|
|
332
357
|
con.print(Rule(f"cctx — {project.display_name}"))
|
|
333
358
|
if not project.sessions:
|
|
@@ -339,14 +364,20 @@ def render_sessions(project: ProjectInfo, *, console: Console | None = None) ->
|
|
|
339
364
|
table.add_column("Date", style="dim")
|
|
340
365
|
table.add_column("Branch", style="dim")
|
|
341
366
|
table.add_column("Path", style="dim")
|
|
367
|
+
table.add_column("Status")
|
|
342
368
|
|
|
343
369
|
for s in project.sessions:
|
|
344
370
|
date_str = s.start_time.strftime("%Y-%m-%d %H:%M") if s.start_time else "—"
|
|
371
|
+
if s.session_id in _live:
|
|
372
|
+
status_cell = Text(f"● {_live[s.session_id]}", style="green bold")
|
|
373
|
+
else:
|
|
374
|
+
status_cell = Text("")
|
|
345
375
|
table.add_row(
|
|
346
376
|
s.session_id[:8],
|
|
347
377
|
date_str,
|
|
348
378
|
s.git_branch or "—",
|
|
349
379
|
str(s.path),
|
|
380
|
+
status_cell,
|
|
350
381
|
)
|
|
351
382
|
con.print(table)
|
|
352
383
|
con.print()
|