cctx-cli 1.2.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 (169) hide show
  1. cctx_cli-1.4.0/CHANGELOG.md +230 -0
  2. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/__init__.py +1 -1
  4. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/cli.py +48 -10
  5. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/aggregate.py +9 -6
  6. cctx_cli-1.4.0/cctx/diagnostician/patterns/project_specific.py +179 -0
  7. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/harvest.py +187 -18
  8. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/models.py +15 -1
  9. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/recommender/claude_md.py +25 -2
  10. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/terminal.py +32 -12
  11. cctx_cli-1.4.0/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +860 -0
  12. cctx_cli-1.4.0/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +1312 -0
  13. cctx_cli-1.4.0/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +214 -0
  14. cctx_cli-1.4.0/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +235 -0
  15. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/pyproject.toml +1 -1
  16. cctx_cli-1.4.0/tests/diagnostician/test_project_specific.py +218 -0
  17. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_aggregate.py +6 -5
  18. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_cli.py +129 -0
  19. cctx_cli-1.4.0/tests/test_harvest_check.py +418 -0
  20. cctx_cli-1.4.0/tests/test_models_project_pattern.py +37 -0
  21. cctx_cli-1.4.0/tests/test_recommender.py +56 -0
  22. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_terminal_renderer.py +74 -0
  23. cctx_cli-1.2.0/CHANGELOG.md +0 -106
  24. cctx_cli-1.2.0/tests/test_harvest_check.py +0 -139
  25. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.github/workflows/ci.yml +0 -0
  26. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.github/workflows/publish.yml +0 -0
  27. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.github/workflows/release.yml +0 -0
  28. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/.gitignore +0 -0
  29. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/CLAUDE.md +0 -0
  30. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/DESIGN.md +0 -0
  31. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/PRODUCT.md +0 -0
  32. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/README.md +0 -0
  33. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/action.yml +0 -0
  34. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/__init__.py +0 -0
  35. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/inflection.py +0 -0
  36. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  37. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  38. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  39. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  40. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  41. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  42. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/discovery.py +0 -0
  43. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/__init__.py +0 -0
  44. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/csv.py +0 -0
  45. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/json.py +0 -0
  46. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/exporters/jsonl.py +0 -0
  47. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/parsers/__init__.py +0 -0
  48. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/parsers/claude_code.py +0 -0
  49. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/pricing.py +0 -0
  50. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/recommender/__init__.py +0 -0
  51. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/recommender/evidence.py +0 -0
  52. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/__init__.py +0 -0
  53. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/github.py +0 -0
  54. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/report.py +0 -0
  55. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  56. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/renderers/trace_tui.py +0 -0
  57. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/tokenizer.py +0 -0
  58. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx/watcher.py +0 -0
  59. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/cctx-project-brief.md +0 -0
  60. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/demo.gif +0 -0
  61. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/demo.tape +0 -0
  62. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  63. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  64. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  65. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  66. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  67. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  68. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  69. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  70. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  71. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  72. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  73. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/__init__.py +0 -0
  74. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/conftest.py +0 -0
  75. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/__init__.py +0 -0
  76. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/conftest.py +0 -0
  77. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_dead_end.py +0 -0
  78. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_inflection.py +0 -0
  79. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_orchestrator.py +0 -0
  80. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_retry_loop.py +0 -0
  81. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_scope_creep.py +0 -0
  82. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_stale_context.py +0 -0
  83. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  84. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/exporters/__init__.py +0 -0
  85. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/exporters/test_csv.py +0 -0
  86. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/exporters/test_jsonl.py +0 -0
  87. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/README.md +0 -0
  88. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  89. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  90. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  91. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  92. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  93. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  94. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  95. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  96. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  97. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  98. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  99. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  100. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  101. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  102. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  103. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  104. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  105. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  106. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  107. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  108. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  109. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  110. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  111. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  112. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  113. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  114. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  115. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  116. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  117. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  118. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  119. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  120. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  121. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  122. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  123. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  124. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  125. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  126. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  127. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  128. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  129. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  130. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  131. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  132. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  133. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  134. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  135. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  136. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  137. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  138. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  139. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  140. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  141. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  142. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  143. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  144. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  145. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  146. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/scrub.py +0 -0
  147. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  148. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  149. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  150. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  151. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  152. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/parsers/__init__.py +0 -0
  153. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code.py +0 -0
  154. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code_integration.py +0 -0
  155. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/recommender/__init__.py +0 -0
  156. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/recommender/test_claude_md.py +0 -0
  157. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/recommender/test_evidence.py +0 -0
  158. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/renderers/__init__.py +0 -0
  159. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/renderers/test_report.py +0 -0
  160. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  161. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_cli_export.py +0 -0
  162. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_discovery.py +0 -0
  163. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_github_summary.py +0 -0
  164. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_harvest.py +0 -0
  165. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_models.py +0 -0
  166. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_smoke.py +0 -0
  167. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_tokenizer.py +0 -0
  168. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_trace_tui.py +0 -0
  169. {cctx_cli-1.2.0 → cctx_cli-1.4.0}/tests/test_watcher.py +0 -0
@@ -0,0 +1,230 @@
1
+ # CHANGELOG
2
+
3
+ <!-- version list -->
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
+
70
+ ## v1.3.0 (2026-05-17)
71
+
72
+ ### Bug Fixes
73
+
74
+ - Drop unused turn_number from result_map in _find_pairs
75
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
76
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
77
+
78
+ - Restore WHY comment, fix_key != failure_key guard, tighten tuple annotation
79
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
80
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
81
+
82
+ - Ruff lint failures (E501, F401, E741, I001) ([#86](https://github.com/jacquardlabs/cctx/pull/86),
83
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
84
+
85
+ ### Documentation
86
+
87
+ - M14 project-pattern-detection implementation plan
88
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
89
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
90
+
91
+ - M14 project-specific pattern detection design spec
92
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
93
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
94
+
95
+ - Note why harvest --since skips project_specific.detect()
96
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
97
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
98
+
99
+ ### Features
100
+
101
+ - Add ProjectPattern model, AggregateReport.project_patterns, FindingKind.PROJECT_PATTERN
102
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
103
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
104
+
105
+ - Aggregate.run() returns (Diagnosis, SessionTrace) pairs
106
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
107
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
108
+
109
+ - Generate_from_patterns() — CLAUDE.md patches from ProjectPatterns
110
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
111
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
112
+
113
+ - M14 project-specific pattern detection ([#86](https://github.com/jacquardlabs/cctx/pull/86),
114
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
115
+
116
+ - Project_specific.detect() — cross-session failure/fix pattern detector
117
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
118
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
119
+
120
+ - Render_aggregate() shows project-specific patterns table
121
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
122
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
123
+
124
+ - Wire project_specific.detect() into autopsy and harvest --since paths
125
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
126
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
127
+
128
+
129
+ ## v1.2.0 (2026-05-17)
130
+
131
+ ### Features
132
+
133
+ - --until DATE, autopsy --json, export --format json (M12 #77 #78 #79)
134
+ ([#84](https://github.com/jacquardlabs/cctx/pull/84),
135
+ [`803b5f1`](https://github.com/jacquardlabs/cctx/commit/803b5f190404679ddef4cbbec7478d04c57b8413))
136
+
137
+
138
+ ## v1.1.0 (2026-05-17)
139
+
140
+ ### Chores
141
+
142
+ - Add skip-existing to pypi publish action
143
+ ([`23d7e16`](https://github.com/jacquardlabs/cctx/commit/23d7e16e18074da3c25899ba98298100ad3c1ad3))
144
+
145
+ ### Features
146
+
147
+ - M9 polish — verdict headline, --top N, --turn N
148
+ ([#83](https://github.com/jacquardlabs/cctx/pull/83),
149
+ [`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
150
+
151
+ - M9 polish — verdict headline, --top N, and --turn N
152
+ ([#83](https://github.com/jacquardlabs/cctx/pull/83),
153
+ [`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
154
+
155
+ ### Refactoring
156
+
157
+ - Cache verdict, fix markup=False bug, use reverse=True
158
+ ([#83](https://github.com/jacquardlabs/cctx/pull/83),
159
+ [`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
160
+
161
+
162
+ ## v1.0.0 (2026-05-17)
163
+
164
+ ### Continuous Integration
165
+
166
+ - Add python-semantic-release for fully automated CD
167
+ ([`9844921`](https://github.com/jacquardlabs/cctx/commit/98449213e5b3bd597c47d54e4d5043e245adafe4))
168
+
169
+ - Add workflow_dispatch to release.yml for manual trigger
170
+ ([`08ac9f8`](https://github.com/jacquardlabs/cctx/commit/08ac9f80eb6ceee7b57155852febc4274cbaf3b0))
171
+
172
+
173
+ ## v0.2.0 (2026-05-16)
174
+
175
+ ### Bug Fixes
176
+
177
+ - Ruff lint — B904, E402, E501, F841 across cli, tests, and renderers
178
+ ([`fa7105f`](https://github.com/jacquardlabs/cctx/commit/fa7105fef340d89136f1996b86824e30d080a730))
179
+
180
+ - Trace TUI token sum within line-length limit
181
+ ([`5b7416c`](https://github.com/jacquardlabs/cctx/commit/5b7416c5b92790cfe66f1e53f20891ecaf6e03b0))
182
+
183
+ ### Chores
184
+
185
+ - Bump version to 0.2.0, update PRODUCT.md and CLAUDE.md
186
+ ([`828ed49`](https://github.com/jacquardlabs/cctx/commit/828ed4997df9f4a264669bc38f4b10588a151f1c))
187
+
188
+ ### Documentation
189
+
190
+ - Add CI usage section clarifying harvest is local-only
191
+ ([`c526408`](https://github.com/jacquardlabs/cctx/commit/c526408a35749858e1c0b0b6ba42aea95bb8f621))
192
+
193
+ ### Features
194
+
195
+ - **#64,#63**: Tool-thrash and dead-end exploration classifiers
196
+ ([`14f8f45`](https://github.com/jacquardlabs/cctx/commit/14f8f45f9d3f4ef2fefac374d3e4cea36185c60d))
197
+
198
+ - **#65**: Harvest v2 — route patches to any .md target (.claude/rules/, .claude/skills/)
199
+ ([`06ef9b7`](https://github.com/jacquardlabs/cctx/commit/06ef9b7a8ae9abacea54c2c826efc3fb6e6e80be))
200
+
201
+ - **#66**: Cctx harvest --check audits CLAUDE.md for dead refs and empty sections
202
+ ([`a3be1d0`](https://github.com/jacquardlabs/cctx/commit/a3be1d0923d387b6830b10c7c2c5acf34a3b8917))
203
+
204
+ - **#67**: Interactive aggregate drill-down; --check docs in README
205
+ ([`3db5429`](https://github.com/jacquardlabs/cctx/commit/3db5429dede40777b23d83e32a4b15a8c0e82a16))
206
+
207
+ - **#68**: --since accepts 7d, 2w, YYYY-MM-DD, and date ranges
208
+ ([`434d7c4`](https://github.com/jacquardlabs/cctx/commit/434d7c448406e0a5465380ed395e1fadaa1c0db1))
209
+
210
+ - **#69**: Annotate costs as estimates (~85–95%) in terminal and HTML output
211
+ ([`5a49889`](https://github.com/jacquardlabs/cctx/commit/5a49889bd43555d30a58c2b78eddf6acbb0d8e97))
212
+
213
+ - **#70**: Cctx watch — live waste signals during an active session
214
+ ([`f533a13`](https://github.com/jacquardlabs/cctx/commit/f533a13e1f087da00457ce7b8215934f20c403a2))
215
+
216
+ - **#72**: Cctx autopsy --github-summary writes findings to GitHub Actions job summary
217
+ ([`df91256`](https://github.com/jacquardlabs/cctx/commit/df91256b58b75a9bbe9594a000b00dee2ac2fbc5))
218
+
219
+ - **#73**: Cctx GitHub Action (composite) + --fail-on-findings flag
220
+ ([`9229584`](https://github.com/jacquardlabs/cctx/commit/9229584ea192ce1b1ee6718721d981cba0ca13e0))
221
+
222
+ ### Refactoring
223
+
224
+ - Consolidate KIND_LABEL, fix private import, clean up watcher tests
225
+ ([`31ad4d7`](https://github.com/jacquardlabs/cctx/commit/31ad4d75885eed9c8238c4eb1c08bd5d1ba51a15))
226
+
227
+
228
+ ## v0.1.0 (2026-05-16)
229
+
230
+ - Initial Release
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.2.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.2.0"
3
+ __version__ = "1.4.0"
@@ -20,6 +20,7 @@ import rich_click as click
20
20
 
21
21
  from cctx import diagnostician
22
22
  from cctx.diagnostician import aggregate
23
+ from cctx.diagnostician.patterns import project_specific
23
24
  from cctx.discovery import complete_project as _complete_project
24
25
  from cctx.models import KIND_LABEL, AggregateReport
25
26
  from cctx.parsers.claude_code import parse_session
@@ -146,22 +147,32 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
146
147
  from rich.console import Console
147
148
  from rich.rule import Rule
148
149
 
150
+ from cctx.harvest import CheckIssue, CheckSeverity
151
+
149
152
  con = Console()
150
153
  claude_md_path = target_dir / "CLAUDE.md"
151
154
  con.print(Rule(f"cctx harvest --check — {claude_md_path}"))
152
155
  if not findings:
153
- con.print("✓ CLAUDE.md looks clean — no dead references or empty sections.")
156
+ con.print("✓ CLAUDE.md looks clean — no issues found.")
154
157
  return
155
158
  con.print(f"{len(findings)} issue(s) found:\n")
156
- from cctx.harvest import CheckIssue
157
159
  _ISSUE_LABEL = {
158
- CheckIssue.DEAD_FILE_REF: "dead file reference",
159
- CheckIssue.DEAD_SKILL_REF: "dead skill reference",
160
- 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]",
161
171
  }
162
172
  for f in findings:
173
+ badge = _SEV_BADGE.get(f.severity, " ")
163
174
  label = _ISSUE_LABEL.get(f.issue, f.issue.value)
164
- con.print(f" [{f.heading}] {label}: {f.detail}")
175
+ con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
165
176
 
166
177
 
167
178
  @click.group()
@@ -358,11 +369,14 @@ def autopsy(
358
369
  f"Invalid --until date '{until_date}'. Expected YYYY-MM-DD."
359
370
  ) from None
360
371
  label = f"{label} until {until_date.strip()}"
361
- diagnoses = aggregate.run(project_dir, start, end)
372
+ pairs = aggregate.run(project_dir, start, end)
373
+ diagnoses = [d for d, _ in pairs]
362
374
  ev = evidence_mod.accumulate(diagnoses)
363
375
  if top_n is not None:
364
376
  ev = dict(sorted(ev.items(), key=lambda x: x[1].session_count, reverse=True)[:top_n])
365
- patches = claude_md.generate_from_evidence(ev)
377
+ patterns = project_specific.detect(pairs)
378
+ pattern_patches = claude_md.generate_from_patterns(patterns)
379
+ patches = claude_md.generate_from_evidence(ev) + pattern_patches
366
380
  report = AggregateReport(
367
381
  period_label=label,
368
382
  sessions_analysed=len(diagnoses),
@@ -371,6 +385,7 @@ def autopsy(
371
385
  waste_cost_usd=sum(d.waste_cost_usd for d in diagnoses),
372
386
  by_kind=ev,
373
387
  patches=patches,
388
+ project_patterns=patterns,
374
389
  )
375
390
  render_aggregate(report)
376
391
  _aggregate_drilldown(report, diagnoses)
@@ -544,6 +559,14 @@ def trace(target: Path | None, latest: bool) -> None:
544
559
  default=False,
545
560
  help="Audit existing CLAUDE.md for dead references and empty sections. Exit 1 if findings.",
546
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
+ )
547
570
  def harvest(
548
571
  target: Path,
549
572
  since: str | None,
@@ -551,15 +574,27 @@ def harvest(
551
574
  dry_run: bool,
552
575
  target_dir: Path | None,
553
576
  check_mode: bool,
577
+ check_severity: str,
554
578
  ) -> None:
555
579
  """Apply autopsy patches to CLAUDE.md."""
556
580
  from cctx.harvest import apply_patches, check_claude_md, preview_patches
557
581
 
558
582
  if check_mode:
583
+ from cctx.harvest import CheckSeverity
559
584
  resolved_dir = target_dir or Path.cwd()
560
585
  findings = check_claude_md(resolved_dir)
561
586
  _render_check_findings(findings, resolved_dir)
562
- 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)
563
598
 
564
599
  if apply_mode and dry_run:
565
600
  raise click.UsageError("--apply and --dry-run are mutually exclusive.")
@@ -571,8 +606,11 @@ def harvest(
571
606
  if since is not None:
572
607
  project_dir = target if target.is_dir() else target.parent
573
608
  start, end, _label = parse_since(since)
574
- diagnoses = aggregate.run(project_dir, start, end)
609
+ pairs = aggregate.run(project_dir, start, end)
610
+ diagnoses = [d for d, _ in pairs]
575
611
  ev = evidence_mod.accumulate(diagnoses)
612
+ # project_specific.detect() intentionally omitted: pattern patches need human review
613
+ # (autopsy shows them; harvest doesn't auto-apply).
576
614
  patches = claude_md.generate_from_evidence(ev)
577
615
  else:
578
616
  if target.is_dir():
@@ -1,10 +1,11 @@
1
1
  """Cross-session aggregator.
2
2
 
3
- run(project_dir, start, end) -> list[Diagnosis]
3
+ run(project_dir, start, end) -> list[tuple[Diagnosis, SessionTrace]]
4
4
 
5
5
  Discovers session JSONL files in project_dir modified within [start, end],
6
- parses each one, runs the per-session diagnostician, and returns the list of
7
- Diagnoses. The CLI orchestrates the recommender call separately.
6
+ parses each one, runs the per-session diagnostician, and returns
7
+ (Diagnosis, SessionTrace) pairs. The CLI orchestrates recommender and
8
+ project-specific detection separately.
8
9
  """
9
10
  from __future__ import annotations
10
11
 
@@ -17,12 +18,14 @@ from cctx.parsers.claude_code import parse_session
17
18
  from cctx.tokenizer import tokenize_session
18
19
 
19
20
  if TYPE_CHECKING:
20
- from cctx.models import Diagnosis
21
+ from cctx.models import Diagnosis, SessionTrace
21
22
 
22
23
  UTC = timezone.utc
23
24
 
24
25
 
25
- def run(project_dir: Path, start: datetime, end: datetime) -> list[Diagnosis]:
26
+ def run(
27
+ project_dir: Path, start: datetime, end: datetime
28
+ ) -> list[tuple[Diagnosis, SessionTrace]]:
26
29
  paths = sorted(project_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime)
27
30
 
28
31
  result = []
@@ -33,7 +36,7 @@ def run(project_dir: Path, start: datetime, end: datetime) -> list[Diagnosis]:
33
36
  try:
34
37
  trace = tokenize_session(parse_session(path))
35
38
  diagnosis = diagnostician.run(trace)
36
- result.append(diagnosis)
39
+ result.append((diagnosis, trace))
37
40
  except Exception:
38
41
  continue # skip corrupt sessions; don't fail the whole run
39
42
  return result
@@ -0,0 +1,179 @@
1
+ """Project-specific pattern detector.
2
+
3
+ detect(pairs) -> list[ProjectPattern]
4
+
5
+ Finds (tool_name, failure_key, fix_key) triples that recur in 3+ sessions.
6
+ Bash normalization uses first 3 tokens for cross-session fuzzy matching
7
+ (intentionally looser than retry_loop). No LLM calls.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections import defaultdict
13
+ from typing import TYPE_CHECKING
14
+
15
+ from cctx.models import ProjectPattern
16
+ from cctx.pricing import price_per_tok
17
+
18
+ if TYPE_CHECKING:
19
+ from cctx.models import Diagnosis, SessionTrace, ToolResult
20
+
21
+ MIN_SESSIONS = 3
22
+ FIX_WINDOW = 10 # turns after last failure to search for the fix
23
+
24
+
25
+ def _normalize_key(tool_name: str, tool_input: dict) -> str:
26
+ match tool_name:
27
+ case "Bash":
28
+ tokens = tool_input.get("command", "").strip().split()
29
+ return " ".join(tokens[:3])
30
+ case "Edit" | "Read" | "Write":
31
+ return tool_input.get("file_path", "")
32
+ case "Grep" | "Glob":
33
+ return tool_input.get("pattern", "")
34
+ case _:
35
+ return json.dumps(tool_input, sort_keys=True)
36
+
37
+
38
+ def _is_error(result: ToolResult) -> bool:
39
+ if result.is_error:
40
+ return True
41
+ c = result.content
42
+ return c.startswith("Error:") or c.startswith("error:") or c.startswith("FAILED")
43
+
44
+
45
+ def _find_pairs(trace: SessionTrace) -> list[dict]:
46
+ """Find failure/fix pairs within one session."""
47
+ result_map: dict[str, ToolResult] = {}
48
+ for turn in trace.turns:
49
+ for tr in turn.tool_results:
50
+ result_map[tr.tool_use_id] = tr
51
+
52
+ records = []
53
+ for turn in trace.turns:
54
+ if turn.role != "assistant":
55
+ continue
56
+ for tu in turn.tool_uses:
57
+ result = result_map.get(tu.tool_use_id)
58
+ if result is None:
59
+ continue
60
+ key = _normalize_key(tu.tool_name, tu.tool_input)
61
+ records.append({
62
+ "tool_name": tu.tool_name,
63
+ "key": key,
64
+ "turn": turn.turn_number,
65
+ "is_error": _is_error(result),
66
+ })
67
+
68
+ groups: dict[tuple, list] = defaultdict(list)
69
+ for r in records:
70
+ groups[(r["tool_name"], r["key"])].append(r)
71
+
72
+ found: list[dict] = []
73
+ seen_pairs: set[tuple] = set()
74
+
75
+ for (tool_name, failure_key), group in groups.items():
76
+ errors = [r for r in group if r["is_error"]]
77
+ if len(errors) < 2:
78
+ continue
79
+
80
+ first_err_turn = errors[0]["turn"]
81
+ last_err_turn = errors[-1]["turn"]
82
+
83
+ intervening = any(
84
+ r for r in group
85
+ if not r["is_error"] and first_err_turn < r["turn"] < last_err_turn
86
+ )
87
+ if intervening:
88
+ continue
89
+
90
+ fix = next(
91
+ (
92
+ r for r in records
93
+ if r["tool_name"] == tool_name
94
+ and not r["is_error"]
95
+ and r["key"] != failure_key
96
+ and last_err_turn < r["turn"] <= last_err_turn + FIX_WINDOW
97
+ ),
98
+ None,
99
+ )
100
+ if fix is None:
101
+ continue
102
+
103
+ pair_key = (tool_name, failure_key, fix["key"])
104
+ if pair_key in seen_pairs:
105
+ continue
106
+ seen_pairs.add(pair_key)
107
+
108
+ found.append({
109
+ "tool_name": tool_name,
110
+ "failure_key": failure_key,
111
+ "fix_key": fix["key"],
112
+ "first_failure_turn": first_err_turn,
113
+ "fix_turn": fix["turn"],
114
+ })
115
+
116
+ return found
117
+
118
+
119
+ def _compute_waste(trace: SessionTrace, first_failure_turn: int, fix_turn: int) -> float:
120
+ price = price_per_tok(trace.primary_model)
121
+ total = 0.0
122
+ for turn in trace.turns:
123
+ if (
124
+ turn.role == "assistant"
125
+ and first_failure_turn <= turn.turn_number <= fix_turn
126
+ and turn.usage is not None
127
+ ):
128
+ total += turn.usage.input_tokens * price
129
+ return round(total, 4)
130
+
131
+
132
+ def detect(pairs: list[tuple[Diagnosis, SessionTrace]]) -> list[ProjectPattern]:
133
+ """Detect recurring failure/fix patterns across sessions."""
134
+ session_records: list[dict] = []
135
+ for _diagnosis, trace in pairs:
136
+ for p in _find_pairs(trace):
137
+ session_records.append({
138
+ "session_id": trace.session_id,
139
+ "tool_name": p["tool_name"],
140
+ "failure_key": p["failure_key"],
141
+ "fix_key": p["fix_key"],
142
+ "first_failure_turn": p["first_failure_turn"],
143
+ "fix_turn": p["fix_turn"],
144
+ "trace": trace,
145
+ })
146
+
147
+ groups: dict[tuple, list] = defaultdict(list)
148
+ for r in session_records:
149
+ groups[(r["tool_name"], r["failure_key"], r["fix_key"])].append(r)
150
+
151
+ result: list[ProjectPattern] = []
152
+ for (tool_name, failure_key, fix_key), records in groups.items():
153
+ seen: dict[str, dict] = {}
154
+ for r in records:
155
+ if r["session_id"] not in seen:
156
+ seen[r["session_id"]] = r
157
+
158
+ if len(seen) < MIN_SESSIONS:
159
+ continue
160
+
161
+ unique = list(seen.values())
162
+ wasted = [r["fix_turn"] - r["first_failure_turn"] for r in unique]
163
+ avg_wasted_turns = sum(wasted) / len(wasted)
164
+ total_waste_usd = sum(
165
+ _compute_waste(r["trace"], r["first_failure_turn"], r["fix_turn"])
166
+ for r in unique
167
+ )
168
+
169
+ result.append(ProjectPattern(
170
+ tool_name=tool_name,
171
+ failure_key=failure_key,
172
+ fix_key=fix_key,
173
+ session_count=len(seen),
174
+ avg_wasted_turns=round(avg_wasted_turns, 1),
175
+ total_waste_usd=round(total_waste_usd, 4),
176
+ example_sessions=sorted(r["session_id"] for r in unique)[:3],
177
+ ))
178
+
179
+ return result