cctx-cli 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/CHANGELOG.md +65 -0
  2. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/__init__.py +1 -1
  4. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/cli.py +37 -7
  5. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/harvest.py +187 -18
  6. cctx_cli-1.4.0/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +860 -0
  7. cctx_cli-1.4.0/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +214 -0
  8. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/pyproject.toml +1 -1
  9. cctx_cli-1.4.0/tests/test_harvest_check.py +418 -0
  10. cctx_cli-1.3.0/tests/test_harvest_check.py +0 -139
  11. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.github/workflows/ci.yml +0 -0
  12. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.github/workflows/publish.yml +0 -0
  13. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.github/workflows/release.yml +0 -0
  14. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.gitignore +0 -0
  15. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/CLAUDE.md +0 -0
  16. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/DESIGN.md +0 -0
  17. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/PRODUCT.md +0 -0
  18. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/README.md +0 -0
  19. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/action.yml +0 -0
  20. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/__init__.py +0 -0
  21. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/aggregate.py +0 -0
  22. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/inflection.py +0 -0
  23. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  24. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  25. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
  26. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  27. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  28. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  29. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  30. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/discovery.py +0 -0
  31. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/__init__.py +0 -0
  32. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/csv.py +0 -0
  33. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/json.py +0 -0
  34. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/jsonl.py +0 -0
  35. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/models.py +0 -0
  36. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/parsers/__init__.py +0 -0
  37. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/parsers/claude_code.py +0 -0
  38. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/pricing.py +0 -0
  39. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/recommender/__init__.py +0 -0
  40. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/recommender/claude_md.py +0 -0
  41. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/recommender/evidence.py +0 -0
  42. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/__init__.py +0 -0
  43. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/github.py +0 -0
  44. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/report.py +0 -0
  45. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  46. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/terminal.py +0 -0
  47. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/trace_tui.py +0 -0
  48. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/tokenizer.py +0 -0
  49. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/watcher.py +0 -0
  50. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx-project-brief.md +0 -0
  51. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/demo.gif +0 -0
  52. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/demo.tape +0 -0
  53. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  54. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  55. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  56. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  57. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  58. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  59. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  60. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  61. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  62. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  63. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  64. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  65. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  66. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/__init__.py +0 -0
  67. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/conftest.py +0 -0
  68. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/__init__.py +0 -0
  69. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/conftest.py +0 -0
  70. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_dead_end.py +0 -0
  71. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_inflection.py +0 -0
  72. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_orchestrator.py +0 -0
  73. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_project_specific.py +0 -0
  74. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_retry_loop.py +0 -0
  75. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_scope_creep.py +0 -0
  76. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_stale_context.py +0 -0
  77. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  78. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/exporters/__init__.py +0 -0
  79. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/exporters/test_csv.py +0 -0
  80. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/exporters/test_jsonl.py +0 -0
  81. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/README.md +0 -0
  82. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  83. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  84. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  85. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  86. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  87. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  88. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  89. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  90. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  91. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  92. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  93. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  94. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  95. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  96. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  97. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  98. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  99. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  100. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  101. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  102. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  103. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  104. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  105. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  106. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  107. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  108. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  109. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  110. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  111. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  112. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  113. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  114. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  115. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  116. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  117. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  118. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  119. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  120. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  121. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  122. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  123. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  124. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  125. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  126. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  127. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  128. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  129. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  130. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  131. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  132. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  133. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  134. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  135. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  136. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  137. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  138. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  139. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  140. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/scrub.py +0 -0
  141. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  142. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  143. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  144. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  145. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  146. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/parsers/__init__.py +0 -0
  147. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code.py +0 -0
  148. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code_integration.py +0 -0
  149. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/recommender/__init__.py +0 -0
  150. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/recommender/test_claude_md.py +0 -0
  151. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/recommender/test_evidence.py +0 -0
  152. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/renderers/__init__.py +0 -0
  153. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/renderers/test_report.py +0 -0
  154. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  155. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_aggregate.py +0 -0
  156. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_cli.py +0 -0
  157. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_cli_export.py +0 -0
  158. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_discovery.py +0 -0
  159. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_github_summary.py +0 -0
  160. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_harvest.py +0 -0
  161. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_models.py +0 -0
  162. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_models_project_pattern.py +0 -0
  163. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_recommender.py +0 -0
  164. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_smoke.py +0 -0
  165. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_terminal_renderer.py +0 -0
  166. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_tokenizer.py +0 -0
  167. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_trace_tui.py +0 -0
  168. {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_watcher.py +0 -0
@@ -2,6 +2,71 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.4.0 (2026-05-20)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Deduplicate harvest check import, align severity badge output
10
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
11
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
12
+
13
+ - Move defaultdict import to top-level, add _words() return type
14
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
15
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
16
+
17
+ - Use removeprefix instead of lstrip to preserve .claude/skills/ dot prefix
18
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
19
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
20
+
21
+ ### Documentation
22
+
23
+ - M15 harvest --check depth design spec ([#87](https://github.com/jacquardlabs/cctx/pull/87),
24
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
25
+
26
+ - M15 harvest --check depth implementation plan
27
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
28
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
29
+
30
+ ### Features
31
+
32
+ - --check-severity flag and severity badges in harvest --check output
33
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
34
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
35
+
36
+ - Check_contradictions() — always/never keyword heuristic
37
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
38
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
39
+
40
+ - Check_redundancy() — Jaccard similarity ≥ 0.8 on section word sets
41
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
42
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
43
+
44
+ - Check_staleness() — backtick function refs grepped against project source
45
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
46
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
47
+
48
+ - CheckSeverity enum, severity field on CheckFinding, new CheckIssue values
49
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
50
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
51
+
52
+ - Harvest --check depth — contradiction, redundancy, staleness detectors + --check-severity
53
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
54
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
55
+
56
+ - Wire all four detectors into check_claude_md ([#87](https://github.com/jacquardlabs/cctx/pull/87),
57
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
58
+
59
+ ### Refactoring
60
+
61
+ - Check_redundancy — compute _words once per section, remove dead union guard
62
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
63
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
64
+
65
+ - Check_staleness — module-level _STALENESS_EXCLUDED, min-len in regex, per-file search
66
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
67
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
68
+
69
+
5
70
  ## v1.3.0 (2026-05-17)
6
71
 
7
72
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Diagnose Claude Code sessions — find what went wrong, what it cost, and what to add to CLAUDE.md
5
5
  Author: Jacquard Labs
6
6
  License-Expression: MIT
@@ -1,3 +1,3 @@
1
1
  """cctx: profile, debug, and optimize Claude Code and Agent SDK sessions."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.4.0"
@@ -147,22 +147,32 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
147
147
  from rich.console import Console
148
148
  from rich.rule import Rule
149
149
 
150
+ from cctx.harvest import CheckIssue, CheckSeverity
151
+
150
152
  con = Console()
151
153
  claude_md_path = target_dir / "CLAUDE.md"
152
154
  con.print(Rule(f"cctx harvest --check — {claude_md_path}"))
153
155
  if not findings:
154
- con.print("✓ CLAUDE.md looks clean — no dead references or empty sections.")
156
+ con.print("✓ CLAUDE.md looks clean — no issues found.")
155
157
  return
156
158
  con.print(f"{len(findings)} issue(s) found:\n")
157
- from cctx.harvest import CheckIssue
158
159
  _ISSUE_LABEL = {
159
- CheckIssue.DEAD_FILE_REF: "dead file reference",
160
- CheckIssue.DEAD_SKILL_REF: "dead skill reference",
161
- CheckIssue.EMPTY_SECTION: "empty section",
160
+ CheckIssue.DEAD_FILE_REF: "dead file reference",
161
+ CheckIssue.DEAD_SKILL_REF: "dead skill reference",
162
+ CheckIssue.EMPTY_SECTION: "empty section",
163
+ CheckIssue.CONTRADICTION: "contradiction",
164
+ CheckIssue.REDUNDANCY: "redundancy",
165
+ CheckIssue.STALE_IDENTIFIER: "stale identifier",
166
+ }
167
+ _SEV_BADGE = {
168
+ CheckSeverity.HIGH: "[HIGH]",
169
+ CheckSeverity.MEDIUM: "[MED]",
170
+ CheckSeverity.LOW: "[LOW]",
162
171
  }
163
172
  for f in findings:
173
+ badge = _SEV_BADGE.get(f.severity, " ")
164
174
  label = _ISSUE_LABEL.get(f.issue, f.issue.value)
165
- con.print(f" [{f.heading}] {label}: {f.detail}")
175
+ con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
166
176
 
167
177
 
168
178
  @click.group()
@@ -549,6 +559,14 @@ def trace(target: Path | None, latest: bool) -> None:
549
559
  default=False,
550
560
  help="Audit existing CLAUDE.md for dead references and empty sections. Exit 1 if findings.",
551
561
  )
562
+ @click.option(
563
+ "--check-severity",
564
+ "check_severity",
565
+ default="MEDIUM",
566
+ type=click.Choice(["LOW", "MEDIUM", "HIGH"], case_sensitive=False),
567
+ show_default=True,
568
+ help="Minimum severity that causes --check to exit 1.",
569
+ )
552
570
  def harvest(
553
571
  target: Path,
554
572
  since: str | None,
@@ -556,15 +574,27 @@ def harvest(
556
574
  dry_run: bool,
557
575
  target_dir: Path | None,
558
576
  check_mode: bool,
577
+ check_severity: str,
559
578
  ) -> None:
560
579
  """Apply autopsy patches to CLAUDE.md."""
561
580
  from cctx.harvest import apply_patches, check_claude_md, preview_patches
562
581
 
563
582
  if check_mode:
583
+ from cctx.harvest import CheckSeverity
564
584
  resolved_dir = target_dir or Path.cwd()
565
585
  findings = check_claude_md(resolved_dir)
566
586
  _render_check_findings(findings, resolved_dir)
567
- raise SystemExit(1 if findings else 0)
587
+ _SEVERITY_ORDER = {
588
+ CheckSeverity.LOW: 0,
589
+ CheckSeverity.MEDIUM: 1,
590
+ CheckSeverity.HIGH: 2,
591
+ }
592
+ threshold = CheckSeverity(check_severity.lower())
593
+ triggering = [
594
+ f for f in findings
595
+ if _SEVERITY_ORDER[f.severity] >= _SEVERITY_ORDER[threshold]
596
+ ]
597
+ raise SystemExit(1 if triggering else 0)
568
598
 
569
599
  if apply_mode and dry_run:
570
600
  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 = "dead_file_ref" # backtick-quoted path that doesn't exist on disk
33
- DEAD_SKILL_REF = "dead_skill_ref" # .claude/skills/ reference that doesn't exist
34
- EMPTY_SECTION = "empty_section" # ## heading with no content
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: str # ## section where this was found ("(preamble)" if before first heading)
40
- issue: CheckIssue
41
- detail: str # human-readable description
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 check_claude_md(target_dir: Path) -> list[CheckFinding]:
237
- """Audit CLAUDE.md in target_dir for deterministically detectable issues.
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
- Checks:
240
- - Dead file references: backtick-quoted paths that don't exist on disk
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 an empty list if CLAUDE.md doesn't exist (not an error).
274
+ Returns findings for each contradiction found (severity: HIGH).
245
275
  """
246
- claude_md = target_dir / "CLAUDE.md"
247
- if not claude_md.exists():
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
- content = claude_md.read_text(encoding="utf-8")
251
- sections = _parse_sections(content)
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).lstrip("./")
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
+ )