cctx-cli 1.5.1__tar.gz → 1.6.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.5.1 → cctx_cli-1.6.0}/CHANGELOG.md +59 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/PKG-INFO +1 -1
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/PRODUCT.md +1 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/cli.py +36 -1
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/harvest.py +74 -4
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/models.py +16 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +22 -6
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/pyproject.toml +1 -1
- cctx_cli-1.6.0/tests/test_harvest_emit.py +204 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/.gitignore +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/CLAUDE.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/DESIGN.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/README.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/action.yml +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/agents.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/recommender/claude_md.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/renderers/terminal.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/demo.gif +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/demo.tape +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/conftest.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_models.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.5.1 → cctx_cli-1.6.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.6.0 (2026-06-10)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Harvest — preview_patches dedup per (target, heading) not heading-only
|
|
10
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
11
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
12
|
+
|
|
13
|
+
- Harvest — shorten local-import comment under 100-char line limit
|
|
14
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
15
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
- Harvest — correct misleading local-import comment
|
|
20
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
21
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
22
|
+
|
|
23
|
+
- Spec deviation note (sync returns patches) + PRODUCT.md cross-agent emit row
|
|
24
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
25
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
- Cctx harvest --emit — cross-agent layer to AGENTS.md (#82)
|
|
30
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
31
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
32
|
+
|
|
33
|
+
- Cli — harvest --emit / --sync cross-agent emit
|
|
34
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
35
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
36
|
+
|
|
37
|
+
- Harvest — EMIT_TARGETS + retarget_patches (fan-out to AGENTS.md)
|
|
38
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
39
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
40
|
+
|
|
41
|
+
- Harvest — sync_managed_sections backfills CLAUDE.md into emit target
|
|
42
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
43
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
44
|
+
|
|
45
|
+
- Models — MANAGED_HEADINGS registry for cctx-owned CLAUDE.md sections
|
|
46
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
47
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
48
|
+
|
|
49
|
+
### Testing
|
|
50
|
+
|
|
51
|
+
- Emit + sync idempotency through apply_patches
|
|
52
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
53
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
54
|
+
|
|
55
|
+
- End-to-end fan-out to both targets; spec: reconcile sync error contract
|
|
56
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
57
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
58
|
+
|
|
59
|
+
- Lock MANAGED_HEADINGS registry to recommender templates
|
|
60
|
+
([#108](https://github.com/jacquardlabs/cctx/pull/108),
|
|
61
|
+
[`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
|
|
62
|
+
|
|
63
|
+
|
|
5
64
|
## v1.5.1 (2026-06-10)
|
|
6
65
|
|
|
7
66
|
### Bug Fixes
|
|
@@ -76,6 +76,7 @@ Six commands (`ls`, `autopsy`, `harvest`, `watch`, `trace`, `export`). No comman
|
|
|
76
76
|
| Memory-hygiene depth | `harvest --check` + `--check-severity` | v1.4.0 (M13) |
|
|
77
77
|
| Live session badges | `cctx ls` | unreleased |
|
|
78
78
|
| Live session detection, early idle exit | `cctx watch` | unreleased |
|
|
79
|
+
| Cross-agent emit | `cctx harvest --emit agents [--sync]` | M15; mirror CLAUDE.md sections to AGENTS.md — unreleased |
|
|
79
80
|
|
|
80
81
|
### Pattern classifiers (v1.4.0)
|
|
81
82
|
|
|
@@ -23,6 +23,7 @@ from cctx.agents import live_sessions as _live_sessions
|
|
|
23
23
|
from cctx.diagnostician import aggregate
|
|
24
24
|
from cctx.diagnostician.patterns import project_specific
|
|
25
25
|
from cctx.discovery import complete_project as _complete_project
|
|
26
|
+
from cctx.harvest import EMIT_TARGETS
|
|
26
27
|
from cctx.models import KIND_LABEL, AggregateReport
|
|
27
28
|
from cctx.parsers.claude_code import parse_session
|
|
28
29
|
from cctx.recommender import claude_md
|
|
@@ -570,6 +571,22 @@ def trace(target: Path | None, latest: bool) -> None:
|
|
|
570
571
|
show_default=True,
|
|
571
572
|
help="Minimum severity that causes --check to exit 1.",
|
|
572
573
|
)
|
|
574
|
+
@click.option(
|
|
575
|
+
"--emit",
|
|
576
|
+
"emit_targets",
|
|
577
|
+
multiple=True,
|
|
578
|
+
type=click.Choice(list(EMIT_TARGETS)),
|
|
579
|
+
help="Also write applicable patches to another agent's instruction file "
|
|
580
|
+
"(e.g. AGENTS.md). Repeatable.",
|
|
581
|
+
)
|
|
582
|
+
@click.option(
|
|
583
|
+
"--sync",
|
|
584
|
+
"sync_mode",
|
|
585
|
+
is_flag=True,
|
|
586
|
+
default=False,
|
|
587
|
+
help="With --emit: also mirror already-harvested cctx-managed sections "
|
|
588
|
+
"from CLAUDE.md into the emit target.",
|
|
589
|
+
)
|
|
573
590
|
def harvest(
|
|
574
591
|
target: Path,
|
|
575
592
|
since: str | None,
|
|
@@ -578,9 +595,20 @@ def harvest(
|
|
|
578
595
|
target_dir: Path | None,
|
|
579
596
|
check_mode: bool,
|
|
580
597
|
check_severity: str,
|
|
598
|
+
emit_targets: tuple[str, ...],
|
|
599
|
+
sync_mode: bool,
|
|
581
600
|
) -> None:
|
|
582
601
|
"""Apply autopsy patches to CLAUDE.md."""
|
|
583
|
-
from cctx.harvest import
|
|
602
|
+
from cctx.harvest import (
|
|
603
|
+
apply_patches,
|
|
604
|
+
check_claude_md,
|
|
605
|
+
preview_patches,
|
|
606
|
+
retarget_patches,
|
|
607
|
+
sync_managed_sections,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if sync_mode and not emit_targets:
|
|
611
|
+
raise click.UsageError("--sync requires --emit.")
|
|
584
612
|
|
|
585
613
|
if check_mode:
|
|
586
614
|
from cctx.harvest import CheckSeverity
|
|
@@ -626,6 +654,13 @@ def harvest(
|
|
|
626
654
|
diagnosis = claude_md.generate(diagnosis)
|
|
627
655
|
patches = diagnosis.patches
|
|
628
656
|
|
|
657
|
+
base = patches
|
|
658
|
+
for t in emit_targets:
|
|
659
|
+
emitted = retarget_patches(base, t)
|
|
660
|
+
if sync_mode:
|
|
661
|
+
emitted = emitted + sync_managed_sections(resolved_dir, t)
|
|
662
|
+
patches = patches + emitted
|
|
663
|
+
|
|
629
664
|
if not patches:
|
|
630
665
|
render_harvest_results([], dry_run=dry_run)
|
|
631
666
|
return
|
|
@@ -13,6 +13,7 @@ Layering rules (MUST respect):
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import dataclasses
|
|
16
17
|
import re
|
|
17
18
|
from collections import defaultdict
|
|
18
19
|
from dataclasses import dataclass
|
|
@@ -20,6 +21,8 @@ from enum import Enum
|
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import TYPE_CHECKING
|
|
22
23
|
|
|
24
|
+
from cctx.models import MANAGED_HEADING_PREFIX, MANAGED_HEADINGS
|
|
25
|
+
|
|
23
26
|
if TYPE_CHECKING:
|
|
24
27
|
from cctx.models import Patch
|
|
25
28
|
|
|
@@ -105,6 +108,70 @@ def _is_supported_target(patch: Patch) -> bool:
|
|
|
105
108
|
# Public API
|
|
106
109
|
# ---------------------------------------------------------------------------
|
|
107
110
|
|
|
111
|
+
# Maps an --emit target name to the destination filename. Single place to add
|
|
112
|
+
# future targets (Cursor, Windsurf, Copilot) when demand exists.
|
|
113
|
+
EMIT_TARGETS: dict[str, str] = {
|
|
114
|
+
"agents": "AGENTS.md",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def retarget_patches(patches: list[Patch], emit_target: str) -> list[Patch]:
|
|
119
|
+
"""Clone CLAUDE.md-targeted patches to the emit target's file.
|
|
120
|
+
|
|
121
|
+
Only patches whose target_file is exactly "CLAUDE.md" are emitted —
|
|
122
|
+
.claude/rules/ and .claude/skills/ patches are Claude Code-specific and do
|
|
123
|
+
not translate to other agents. Returns clones; inputs are unmodified.
|
|
124
|
+
"""
|
|
125
|
+
dest = EMIT_TARGETS[emit_target]
|
|
126
|
+
return [
|
|
127
|
+
dataclasses.replace(p, target_file=dest)
|
|
128
|
+
for p in patches
|
|
129
|
+
if p.target_file == "CLAUDE.md"
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Reverse map: exact managed heading -> the FindingKind that owns it.
|
|
134
|
+
_HEADING_TO_KIND = {heading: kind for kind, heading in MANAGED_HEADINGS.items()}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def sync_managed_sections(target_dir: Path, emit_target: str) -> list[Patch]:
|
|
138
|
+
"""Build synthetic patches mirroring CLAUDE.md's cctx-managed sections.
|
|
139
|
+
|
|
140
|
+
Reads CLAUDE.md in target_dir, keeps sections whose heading is an exact
|
|
141
|
+
MANAGED_HEADINGS value or starts with MANAGED_HEADING_PREFIX, and returns
|
|
142
|
+
one Patch per kept section targeting the emit file. Returns [] if CLAUDE.md
|
|
143
|
+
is absent. The CLI routes these through preview_patches / apply_patches, so
|
|
144
|
+
idempotency and dry-run come for free from the existing machinery.
|
|
145
|
+
"""
|
|
146
|
+
from cctx.models import FindingKind, Patch # runtime use (Patch is TYPE_CHECKING-only above)
|
|
147
|
+
|
|
148
|
+
claude_md = target_dir / "CLAUDE.md"
|
|
149
|
+
if not claude_md.exists():
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
dest = EMIT_TARGETS[emit_target]
|
|
153
|
+
content = claude_md.read_text(encoding="utf-8")
|
|
154
|
+
patches: list[Patch] = []
|
|
155
|
+
|
|
156
|
+
for heading, body in _parse_sections(content):
|
|
157
|
+
is_fixed = heading in _HEADING_TO_KIND
|
|
158
|
+
is_prefixed = heading.startswith(MANAGED_HEADING_PREFIX)
|
|
159
|
+
if not (is_fixed or is_prefixed):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
kind = _HEADING_TO_KIND[heading] if is_fixed else FindingKind.PROJECT_PATTERN
|
|
163
|
+
diff_lines = [heading] + body.splitlines()
|
|
164
|
+
unified_diff = "\n".join(f"+{line}" for line in diff_lines)
|
|
165
|
+
patches.append(Patch(
|
|
166
|
+
target_file=dest,
|
|
167
|
+
description=heading,
|
|
168
|
+
unified_diff=unified_diff,
|
|
169
|
+
finding_kind=kind,
|
|
170
|
+
evidence_summary="synced from CLAUDE.md",
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
return patches
|
|
174
|
+
|
|
108
175
|
|
|
109
176
|
def apply_patch(patch: Patch, target_dir: Path) -> ApplyResult:
|
|
110
177
|
"""Apply one patch. Never raises — errors go into ApplyResult(status=ERROR)."""
|
|
@@ -161,8 +228,10 @@ def apply_patch(patch: Patch, target_dir: Path) -> ApplyResult:
|
|
|
161
228
|
def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]:
|
|
162
229
|
"""Compute what would happen without writing. Returns APPLIED or SKIPPED."""
|
|
163
230
|
results = []
|
|
164
|
-
# Track
|
|
165
|
-
|
|
231
|
+
# Track (target_path, fingerprint) pairs already "seen" within this preview
|
|
232
|
+
# run (idempotency). Keyed by file so the same heading in two different
|
|
233
|
+
# target files is correctly treated as two independent patches.
|
|
234
|
+
seen_fingerprints: set[tuple[Path, str]] = set()
|
|
166
235
|
|
|
167
236
|
for patch in patches:
|
|
168
237
|
target_path = target_dir / patch.target_file
|
|
@@ -181,7 +250,8 @@ def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]
|
|
|
181
250
|
|
|
182
251
|
content = target_path.read_text(encoding="utf-8") if target_path.exists() else ""
|
|
183
252
|
|
|
184
|
-
|
|
253
|
+
already_seen = fp is not None and (target_path, fp) in seen_fingerprints
|
|
254
|
+
if fp is not None and (_already_present(content, fp) or already_seen):
|
|
185
255
|
results.append(ApplyResult(
|
|
186
256
|
patch=patch,
|
|
187
257
|
status=ApplyStatus.SKIPPED,
|
|
@@ -190,7 +260,7 @@ def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]
|
|
|
190
260
|
))
|
|
191
261
|
else:
|
|
192
262
|
if fp is not None:
|
|
193
|
-
seen_fingerprints.add(fp)
|
|
263
|
+
seen_fingerprints.add((target_path, fp))
|
|
194
264
|
results.append(ApplyResult(
|
|
195
265
|
patch=patch,
|
|
196
266
|
status=ApplyStatus.APPLIED,
|
|
@@ -184,6 +184,22 @@ KIND_LABEL: dict[FindingKind, str] = {
|
|
|
184
184
|
FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
# Maps FindingKind to the exact ## heading emitted by its recommender patch
|
|
188
|
+
# template. Single source of truth for "which CLAUDE.md sections cctx owns."
|
|
189
|
+
# harvest.py imports this (never reaches into recommender/) so emit/sync can
|
|
190
|
+
# identify cctx-managed sections without depending on the patch generator.
|
|
191
|
+
MANAGED_HEADINGS: dict[FindingKind, str] = {
|
|
192
|
+
FindingKind.RETRY_LOOP: "## Retry discipline",
|
|
193
|
+
FindingKind.SCOPE_CREEP: "## Scope discipline",
|
|
194
|
+
FindingKind.STALE_CONTEXT: "## Context hygiene",
|
|
195
|
+
FindingKind.TOOL_THRASH: "## Tool-call discipline",
|
|
196
|
+
FindingKind.DEAD_END: "## Exploration discipline",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Project-specific patterns use a heading that embeds tool+key, so the managed
|
|
200
|
+
# section is identified by prefix rather than exact match.
|
|
201
|
+
MANAGED_HEADING_PREFIX: str = "## Project-specific: "
|
|
202
|
+
|
|
187
203
|
|
|
188
204
|
class Severity(str, Enum):
|
|
189
205
|
HIGH = "high"
|
{cctx_cli-1.5.1 → cctx_cli-1.6.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md
RENAMED
|
@@ -88,12 +88,21 @@ Clones patches suitable for emission:
|
|
|
88
88
|
reach here given the above filter, but stated explicitly for clarity).
|
|
89
89
|
- Returns clones via `dataclasses.replace(patch, target_file=EMIT_TARGETS[emit_target])`.
|
|
90
90
|
|
|
91
|
-
### `sync_managed_sections(target_dir, emit_target) -> list[
|
|
91
|
+
### `sync_managed_sections(target_dir, emit_target) -> list[Patch]`
|
|
92
|
+
|
|
93
|
+
> **Implementation deviation (2026-06-10):** This function returns
|
|
94
|
+
> `list[Patch]` rather than applying patches inline (the original draft returned
|
|
95
|
+
> `list[ApplyResult]` and called `apply_patch` itself). The CLI appends these
|
|
96
|
+
> patches to the same list it routes through `preview_patches` / `apply_patches`.
|
|
97
|
+
> Returning patches keeps `--dry-run` write-free by construction and matches the
|
|
98
|
+
> codebase's "CLI decides preview vs. apply" layering — applying inline could not
|
|
99
|
+
> preview, contradicting the `--dry-run` requirement and `test_dry_run_no_writes`.
|
|
92
100
|
|
|
93
101
|
1. Reads `CLAUDE.md` from `target_dir`. Returns empty list if absent.
|
|
94
102
|
2. Calls `_parse_sections(content)` (already in `harvest.py`).
|
|
95
103
|
3. Keeps sections whose heading is exactly in `MANAGED_HEADINGS.values()` OR
|
|
96
|
-
starts with `MANAGED_HEADING_PREFIX`.
|
|
104
|
+
starts with `MANAGED_HEADING_PREFIX`. The leading `("(preamble)", …)` pair
|
|
105
|
+
matches neither branch and is skipped.
|
|
97
106
|
4. For each kept section, constructs a synthetic `Patch` with:
|
|
98
107
|
- `target_file = EMIT_TARGETS[emit_target]`
|
|
99
108
|
- `unified_diff = "\n".join(f"+{line}" for line in [heading] + body.splitlines())`
|
|
@@ -102,8 +111,10 @@ Clones patches suitable for emission:
|
|
|
102
111
|
`FindingKind.PROJECT_PATTERN` for `## Project-specific: …` prefixed headings
|
|
103
112
|
- `description = heading`
|
|
104
113
|
- `evidence_summary = "synced from CLAUDE.md"`
|
|
105
|
-
5.
|
|
106
|
-
|
|
114
|
+
5. Returns the list of synthetic patches. The CLI routes them through the
|
|
115
|
+
existing `preview_patches` / `apply_patches` machinery, which handles
|
|
116
|
+
idempotency via `_already_present` (the `## Heading` line is the fingerprint)
|
|
117
|
+
and dry-run preview without writing.
|
|
107
118
|
|
|
108
119
|
---
|
|
109
120
|
|
|
@@ -143,8 +154,13 @@ under their full path.
|
|
|
143
154
|
## Error contract
|
|
144
155
|
|
|
145
156
|
- Never raises. All failures return `ApplyResult(status=ERROR, message=...)`.
|
|
146
|
-
- `--sync` with no CLAUDE.md: returns `[]` (not an
|
|
147
|
-
|
|
157
|
+
- `--sync` with no CLAUDE.md: `sync_managed_sections` returns `[]` (not an
|
|
158
|
+
error). Because it returns patches rather than applying inline (see the
|
|
159
|
+
deviation note above), there is no `SKIPPED` line for the missing file — the
|
|
160
|
+
empty result simply contributes nothing, and if no other patches exist the CLI
|
|
161
|
+
prints its standard "No patches to apply." message. (The original draft
|
|
162
|
+
emitted a "CLAUDE.md not found — nothing to sync." line; that belonged to the
|
|
163
|
+
inline-apply design and no longer applies.)
|
|
148
164
|
- Emit target directory is created by `apply_patch`'s existing `parent.mkdir`.
|
|
149
165
|
|
|
150
166
|
---
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cctx-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.6.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,204 @@
|
|
|
1
|
+
"""Tests for cctx/harvest.py cross-agent emit (M15) and the managed-heading registry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_managed_headings_cover_the_five_diagnostic_kinds():
|
|
6
|
+
from cctx.models import MANAGED_HEADINGS, FindingKind
|
|
7
|
+
assert MANAGED_HEADINGS == {
|
|
8
|
+
FindingKind.RETRY_LOOP: "## Retry discipline",
|
|
9
|
+
FindingKind.SCOPE_CREEP: "## Scope discipline",
|
|
10
|
+
FindingKind.STALE_CONTEXT: "## Context hygiene",
|
|
11
|
+
FindingKind.TOOL_THRASH: "## Tool-call discipline",
|
|
12
|
+
FindingKind.DEAD_END: "## Exploration discipline",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_managed_heading_prefix_is_project_specific():
|
|
17
|
+
from cctx.models import MANAGED_HEADING_PREFIX
|
|
18
|
+
assert MANAGED_HEADING_PREFIX == "## Project-specific: "
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_registry_matches_templates():
|
|
22
|
+
"""Each MANAGED_HEADINGS value equals the first '+##' line of its template diff."""
|
|
23
|
+
from cctx.models import MANAGED_HEADINGS
|
|
24
|
+
from cctx.recommender.claude_md import _TEMPLATES
|
|
25
|
+
for kind, heading in MANAGED_HEADINGS.items():
|
|
26
|
+
assert kind in _TEMPLATES, f"{kind} missing from _TEMPLATES"
|
|
27
|
+
_desc, diff_body, _target = _TEMPLATES[kind]
|
|
28
|
+
first_line = diff_body.splitlines()[0]
|
|
29
|
+
assert first_line == f"+{heading}", (
|
|
30
|
+
f"{kind}: template heading {first_line!r} != registry {('+' + heading)!r}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _patch(target_file="CLAUDE.md", heading="## Retry discipline"):
|
|
35
|
+
from cctx.models import FindingKind, Patch
|
|
36
|
+
return Patch(
|
|
37
|
+
target_file=target_file,
|
|
38
|
+
description="desc",
|
|
39
|
+
unified_diff=f"+{heading}\n+\n+body line",
|
|
40
|
+
finding_kind=FindingKind.RETRY_LOOP,
|
|
41
|
+
evidence_summary="ev",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_retarget_clones_claude_md_patches_to_agents():
|
|
46
|
+
from cctx.harvest import retarget_patches
|
|
47
|
+
out = retarget_patches([_patch()], "agents")
|
|
48
|
+
assert len(out) == 1
|
|
49
|
+
assert out[0].target_file == "AGENTS.md"
|
|
50
|
+
assert out[0].unified_diff == _patch().unified_diff
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_retarget_excludes_non_claude_md_patches():
|
|
54
|
+
from cctx.harvest import retarget_patches
|
|
55
|
+
rules_patch = _patch(target_file=".claude/rules/foo.md")
|
|
56
|
+
out = retarget_patches([_patch(), rules_patch], "agents")
|
|
57
|
+
assert len(out) == 1
|
|
58
|
+
assert out[0].target_file == "AGENTS.md"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_emit_targets_has_agents():
|
|
62
|
+
from cctx.harvest import EMIT_TARGETS
|
|
63
|
+
assert EMIT_TARGETS["agents"] == "AGENTS.md"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_sync_returns_managed_sections_only(tmp_path):
|
|
67
|
+
from cctx.harvest import sync_managed_sections
|
|
68
|
+
(tmp_path / "CLAUDE.md").write_text(
|
|
69
|
+
"# Project\n\n"
|
|
70
|
+
"## Retry discipline\n\nRetry rule body.\n\n"
|
|
71
|
+
"## My hand-written section\n\nNot managed by cctx.\n\n"
|
|
72
|
+
"## Project-specific: Bash(pnpm install)\n\nUse pnpm --filter.\n",
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
)
|
|
75
|
+
patches = sync_managed_sections(tmp_path, "agents")
|
|
76
|
+
headings = {p.unified_diff.splitlines()[0] for p in patches}
|
|
77
|
+
assert "+## Retry discipline" in headings
|
|
78
|
+
assert "+## Project-specific: Bash(pnpm install)" in headings
|
|
79
|
+
assert "+## My hand-written section" not in headings
|
|
80
|
+
assert all(p.target_file == "AGENTS.md" for p in patches)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_sync_finding_kind_reverse_lookup(tmp_path):
|
|
84
|
+
from cctx.harvest import sync_managed_sections
|
|
85
|
+
from cctx.models import FindingKind
|
|
86
|
+
(tmp_path / "CLAUDE.md").write_text(
|
|
87
|
+
"## Context hygiene\n\nbody\n\n"
|
|
88
|
+
"## Project-specific: Bash(x)\n\nbody\n",
|
|
89
|
+
encoding="utf-8",
|
|
90
|
+
)
|
|
91
|
+
patches = sync_managed_sections(tmp_path, "agents")
|
|
92
|
+
by_heading = {p.unified_diff.splitlines()[0]: p.finding_kind for p in patches}
|
|
93
|
+
assert by_heading["+## Context hygiene"] is FindingKind.STALE_CONTEXT
|
|
94
|
+
assert by_heading["+## Project-specific: Bash(x)"] is FindingKind.PROJECT_PATTERN
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_sync_no_claude_md_returns_empty(tmp_path):
|
|
98
|
+
from cctx.harvest import sync_managed_sections
|
|
99
|
+
assert sync_managed_sections(tmp_path, "agents") == []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_emit_apply_then_reapply_is_idempotent(tmp_path):
|
|
103
|
+
from cctx.harvest import ApplyStatus, apply_patches, retarget_patches
|
|
104
|
+
patches = retarget_patches([_patch()], "agents")
|
|
105
|
+
first = apply_patches(patches, tmp_path)
|
|
106
|
+
assert [r.status for r in first] == [ApplyStatus.APPLIED]
|
|
107
|
+
second = apply_patches(patches, tmp_path)
|
|
108
|
+
assert [r.status for r in second] == [ApplyStatus.SKIPPED]
|
|
109
|
+
text = (tmp_path / "AGENTS.md").read_text(encoding="utf-8")
|
|
110
|
+
assert text.count("## Retry discipline") == 1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_sync_apply_then_reapply_is_idempotent(tmp_path):
|
|
114
|
+
from cctx.harvest import ApplyStatus, apply_patches, sync_managed_sections
|
|
115
|
+
(tmp_path / "CLAUDE.md").write_text(
|
|
116
|
+
"## Retry discipline\n\nRetry rule body.\n", encoding="utf-8"
|
|
117
|
+
)
|
|
118
|
+
patches = sync_managed_sections(tmp_path, "agents")
|
|
119
|
+
apply_patches(patches, tmp_path)
|
|
120
|
+
second = apply_patches(patches, tmp_path)
|
|
121
|
+
assert all(r.status is ApplyStatus.SKIPPED for r in second)
|
|
122
|
+
text = (tmp_path / "AGENTS.md").read_text(encoding="utf-8")
|
|
123
|
+
assert text.count("## Retry discipline") == 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_preview_same_heading_different_targets_both_applied(tmp_path):
|
|
127
|
+
"""Two patches with the same heading but different target files must both
|
|
128
|
+
preview as APPLIED — dedup is per-(file, heading), not heading-only."""
|
|
129
|
+
from cctx.harvest import ApplyStatus, preview_patches
|
|
130
|
+
from cctx.models import FindingKind, Patch
|
|
131
|
+
diff = "+## Retry discipline\n+\n+body"
|
|
132
|
+
patches = [
|
|
133
|
+
Patch("CLAUDE.md", "d", diff, FindingKind.RETRY_LOOP, "e"),
|
|
134
|
+
Patch("AGENTS.md", "d", diff, FindingKind.RETRY_LOOP, "e"),
|
|
135
|
+
]
|
|
136
|
+
statuses = [r.status for r in preview_patches(patches, tmp_path)]
|
|
137
|
+
assert statuses == [ApplyStatus.APPLIED, ApplyStatus.APPLIED]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_preview_same_heading_same_target_dedups(tmp_path):
|
|
141
|
+
"""Two patches with the same heading AND same target: second is SKIPPED."""
|
|
142
|
+
from cctx.harvest import ApplyStatus, preview_patches
|
|
143
|
+
from cctx.models import FindingKind, Patch
|
|
144
|
+
diff = "+## Retry discipline\n+\n+body"
|
|
145
|
+
patches = [
|
|
146
|
+
Patch("AGENTS.md", "d", diff, FindingKind.RETRY_LOOP, "e"),
|
|
147
|
+
Patch("AGENTS.md", "d", diff, FindingKind.RETRY_LOOP, "e"),
|
|
148
|
+
]
|
|
149
|
+
statuses = [r.status for r in preview_patches(patches, tmp_path)]
|
|
150
|
+
assert statuses == [ApplyStatus.APPLIED, ApplyStatus.SKIPPED]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_sync_without_emit_errors(tmp_path):
|
|
154
|
+
from click.testing import CliRunner # noqa: I001
|
|
155
|
+
from cctx.cli import cli
|
|
156
|
+
(tmp_path / "CLAUDE.md").write_text("## Retry discipline\n\nbody\n", encoding="utf-8")
|
|
157
|
+
runner = CliRunner()
|
|
158
|
+
result = runner.invoke(cli, [
|
|
159
|
+
"harvest", str(tmp_path), "--since", "7",
|
|
160
|
+
"--sync", "--target-dir", str(tmp_path),
|
|
161
|
+
])
|
|
162
|
+
assert result.exit_code != 0
|
|
163
|
+
assert "--sync" in result.output and "--emit" in result.output
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_sync_dry_run_writes_nothing(tmp_path):
|
|
167
|
+
from click.testing import CliRunner # noqa: I001
|
|
168
|
+
from cctx.cli import cli
|
|
169
|
+
(tmp_path / "CLAUDE.md").write_text("## Retry discipline\n\nbody\n", encoding="utf-8")
|
|
170
|
+
runner = CliRunner()
|
|
171
|
+
result = runner.invoke(cli, [
|
|
172
|
+
"harvest", str(tmp_path), "--since", "7",
|
|
173
|
+
"--emit", "agents", "--sync", "--dry-run",
|
|
174
|
+
"--target-dir", str(tmp_path),
|
|
175
|
+
])
|
|
176
|
+
assert result.exit_code == 0
|
|
177
|
+
assert not (tmp_path / "AGENTS.md").exists()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_sync_apply_creates_agents_md(tmp_path):
|
|
181
|
+
from click.testing import CliRunner # noqa: I001
|
|
182
|
+
from cctx.cli import cli
|
|
183
|
+
(tmp_path / "CLAUDE.md").write_text("## Retry discipline\n\nbody\n", encoding="utf-8")
|
|
184
|
+
runner = CliRunner()
|
|
185
|
+
result = runner.invoke(cli, [
|
|
186
|
+
"harvest", str(tmp_path), "--since", "7",
|
|
187
|
+
"--emit", "agents", "--sync", "--apply",
|
|
188
|
+
"--target-dir", str(tmp_path),
|
|
189
|
+
])
|
|
190
|
+
assert result.exit_code == 0
|
|
191
|
+
assert (tmp_path / "AGENTS.md").exists()
|
|
192
|
+
assert "## Retry discipline" in (tmp_path / "AGENTS.md").read_text(encoding="utf-8")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_emit_applies_both_targets(tmp_path):
|
|
196
|
+
"""End-to-end fan-out: a CLAUDE.md patch and its retargeted clone both land,
|
|
197
|
+
one in CLAUDE.md and one in AGENTS.md (mirrors the CLI's base+retarget flow)."""
|
|
198
|
+
from cctx.harvest import ApplyStatus, apply_patches, retarget_patches
|
|
199
|
+
base = [_patch()] # one CLAUDE.md patch
|
|
200
|
+
combined = base + retarget_patches(base, "agents")
|
|
201
|
+
results = apply_patches(combined, tmp_path)
|
|
202
|
+
assert all(r.status is ApplyStatus.APPLIED for r in results)
|
|
203
|
+
assert "## Retry discipline" in (tmp_path / "CLAUDE.md").read_text(encoding="utf-8")
|
|
204
|
+
assert "## Retry discipline" in (tmp_path / "AGENTS.md").read_text(encoding="utf-8")
|
|
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
|