cctx-cli 1.2.0__tar.gz → 1.3.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 (165) hide show
  1. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/CHANGELOG.md +59 -0
  2. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/__init__.py +1 -1
  4. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/cli.py +11 -3
  5. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/aggregate.py +9 -6
  6. cctx_cli-1.3.0/cctx/diagnostician/patterns/project_specific.py +179 -0
  7. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/models.py +15 -1
  8. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/recommender/claude_md.py +25 -2
  9. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/terminal.py +32 -12
  10. cctx_cli-1.3.0/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +1312 -0
  11. cctx_cli-1.3.0/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +235 -0
  12. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/pyproject.toml +1 -1
  13. cctx_cli-1.3.0/tests/diagnostician/test_project_specific.py +218 -0
  14. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_aggregate.py +6 -5
  15. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_cli.py +129 -0
  16. cctx_cli-1.3.0/tests/test_models_project_pattern.py +37 -0
  17. cctx_cli-1.3.0/tests/test_recommender.py +56 -0
  18. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_terminal_renderer.py +74 -0
  19. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.github/workflows/ci.yml +0 -0
  20. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.github/workflows/publish.yml +0 -0
  21. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.github/workflows/release.yml +0 -0
  22. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/.gitignore +0 -0
  23. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/CLAUDE.md +0 -0
  24. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/DESIGN.md +0 -0
  25. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/PRODUCT.md +0 -0
  26. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/README.md +0 -0
  27. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/action.yml +0 -0
  28. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/__init__.py +0 -0
  29. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/inflection.py +0 -0
  30. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  31. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  32. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  33. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  34. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  35. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  36. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/discovery.py +0 -0
  37. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/__init__.py +0 -0
  38. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/csv.py +0 -0
  39. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/json.py +0 -0
  40. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/exporters/jsonl.py +0 -0
  41. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/harvest.py +0 -0
  42. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/parsers/__init__.py +0 -0
  43. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/parsers/claude_code.py +0 -0
  44. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/pricing.py +0 -0
  45. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/recommender/__init__.py +0 -0
  46. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/recommender/evidence.py +0 -0
  47. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/__init__.py +0 -0
  48. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/github.py +0 -0
  49. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/report.py +0 -0
  50. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  51. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/renderers/trace_tui.py +0 -0
  52. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/tokenizer.py +0 -0
  53. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx/watcher.py +0 -0
  54. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/cctx-project-brief.md +0 -0
  55. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/demo.gif +0 -0
  56. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/demo.tape +0 -0
  57. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  58. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  59. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  60. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  61. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  62. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  63. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  64. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  65. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  66. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  67. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  68. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/__init__.py +0 -0
  69. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/conftest.py +0 -0
  70. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/__init__.py +0 -0
  71. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/conftest.py +0 -0
  72. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_dead_end.py +0 -0
  73. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_inflection.py +0 -0
  74. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_orchestrator.py +0 -0
  75. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_retry_loop.py +0 -0
  76. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_scope_creep.py +0 -0
  77. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_stale_context.py +0 -0
  78. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  79. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/exporters/__init__.py +0 -0
  80. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/exporters/test_csv.py +0 -0
  81. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/exporters/test_jsonl.py +0 -0
  82. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/README.md +0 -0
  83. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  84. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  85. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  86. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  87. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  88. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  89. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  90. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  91. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  92. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  93. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  94. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  95. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  96. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  97. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  98. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  99. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  100. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  101. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  102. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  103. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  104. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  105. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  106. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  107. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  108. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  109. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  110. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  111. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  112. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  113. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  114. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  115. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  116. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  117. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  118. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  119. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  120. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  121. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  122. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  123. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  124. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  125. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  126. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  127. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  128. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  129. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  130. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  131. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  132. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  133. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  134. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  135. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  136. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  137. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  138. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  139. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  140. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  141. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/scrub.py +0 -0
  142. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  143. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  144. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  145. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  146. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  147. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/parsers/__init__.py +0 -0
  148. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/parsers/test_claude_code.py +0 -0
  149. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/parsers/test_claude_code_integration.py +0 -0
  150. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/recommender/__init__.py +0 -0
  151. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/recommender/test_claude_md.py +0 -0
  152. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/recommender/test_evidence.py +0 -0
  153. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/renderers/__init__.py +0 -0
  154. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/renderers/test_report.py +0 -0
  155. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  156. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_cli_export.py +0 -0
  157. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_discovery.py +0 -0
  158. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_github_summary.py +0 -0
  159. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_harvest.py +0 -0
  160. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_harvest_check.py +0 -0
  161. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_models.py +0 -0
  162. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_smoke.py +0 -0
  163. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_tokenizer.py +0 -0
  164. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_trace_tui.py +0 -0
  165. {cctx_cli-1.2.0 → cctx_cli-1.3.0}/tests/test_watcher.py +0 -0
@@ -2,6 +2,65 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.3.0 (2026-05-17)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Drop unused turn_number from result_map in _find_pairs
10
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
11
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
12
+
13
+ - Restore WHY comment, fix_key != failure_key guard, tighten tuple annotation
14
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
15
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
16
+
17
+ - Ruff lint failures (E501, F401, E741, I001) ([#86](https://github.com/jacquardlabs/cctx/pull/86),
18
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
19
+
20
+ ### Documentation
21
+
22
+ - M14 project-pattern-detection implementation plan
23
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
24
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
25
+
26
+ - M14 project-specific pattern detection design spec
27
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
28
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
29
+
30
+ - Note why harvest --since skips project_specific.detect()
31
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
32
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
33
+
34
+ ### Features
35
+
36
+ - Add ProjectPattern model, AggregateReport.project_patterns, FindingKind.PROJECT_PATTERN
37
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
38
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
39
+
40
+ - Aggregate.run() returns (Diagnosis, SessionTrace) pairs
41
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
42
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
43
+
44
+ - Generate_from_patterns() — CLAUDE.md patches from ProjectPatterns
45
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
46
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
47
+
48
+ - M14 project-specific pattern detection ([#86](https://github.com/jacquardlabs/cctx/pull/86),
49
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
50
+
51
+ - Project_specific.detect() — cross-session failure/fix pattern detector
52
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
53
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
54
+
55
+ - Render_aggregate() shows project-specific patterns table
56
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
57
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
58
+
59
+ - Wire project_specific.detect() into autopsy and harvest --since paths
60
+ ([#86](https://github.com/jacquardlabs/cctx/pull/86),
61
+ [`cefc438`](https://github.com/jacquardlabs/cctx/commit/cefc438f9ff638ba2abf529663b5e24707f03bbb))
62
+
63
+
5
64
  ## v1.2.0 (2026-05-17)
6
65
 
7
66
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.2.0
3
+ Version: 1.3.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.3.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
@@ -358,11 +359,14 @@ def autopsy(
358
359
  f"Invalid --until date '{until_date}'. Expected YYYY-MM-DD."
359
360
  ) from None
360
361
  label = f"{label} until {until_date.strip()}"
361
- diagnoses = aggregate.run(project_dir, start, end)
362
+ pairs = aggregate.run(project_dir, start, end)
363
+ diagnoses = [d for d, _ in pairs]
362
364
  ev = evidence_mod.accumulate(diagnoses)
363
365
  if top_n is not None:
364
366
  ev = dict(sorted(ev.items(), key=lambda x: x[1].session_count, reverse=True)[:top_n])
365
- patches = claude_md.generate_from_evidence(ev)
367
+ patterns = project_specific.detect(pairs)
368
+ pattern_patches = claude_md.generate_from_patterns(patterns)
369
+ patches = claude_md.generate_from_evidence(ev) + pattern_patches
366
370
  report = AggregateReport(
367
371
  period_label=label,
368
372
  sessions_analysed=len(diagnoses),
@@ -371,6 +375,7 @@ def autopsy(
371
375
  waste_cost_usd=sum(d.waste_cost_usd for d in diagnoses),
372
376
  by_kind=ev,
373
377
  patches=patches,
378
+ project_patterns=patterns,
374
379
  )
375
380
  render_aggregate(report)
376
381
  _aggregate_drilldown(report, diagnoses)
@@ -571,8 +576,11 @@ def harvest(
571
576
  if since is not None:
572
577
  project_dir = target if target.is_dir() else target.parent
573
578
  start, end, _label = parse_since(since)
574
- diagnoses = aggregate.run(project_dir, start, end)
579
+ pairs = aggregate.run(project_dir, start, end)
580
+ diagnoses = [d for d, _ in pairs]
575
581
  ev = evidence_mod.accumulate(diagnoses)
582
+ # project_specific.detect() intentionally omitted: pattern patches need human review
583
+ # (autopsy shows them; harvest doesn't auto-apply).
576
584
  patches = claude_md.generate_from_evidence(ev)
577
585
  else:
578
586
  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
@@ -9,7 +9,7 @@ cctx.renderers, cctx.exporters, cctx.tokenizer.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from dataclasses import dataclass
12
+ from dataclasses import dataclass, field
13
13
  from datetime import datetime
14
14
  from enum import Enum
15
15
  from pathlib import Path
@@ -172,6 +172,7 @@ class FindingKind(str, Enum):
172
172
  STALE_CONTEXT = "stale_context"
173
173
  TOOL_THRASH = "tool_thrash"
174
174
  DEAD_END = "dead_end"
175
+ PROJECT_PATTERN = "project_pattern"
175
176
 
176
177
 
177
178
  KIND_LABEL: dict[FindingKind, str] = {
@@ -180,6 +181,7 @@ KIND_LABEL: dict[FindingKind, str] = {
180
181
  FindingKind.STALE_CONTEXT: "STALE CONTEXT",
181
182
  FindingKind.TOOL_THRASH: "TOOL THRASH",
182
183
  FindingKind.DEAD_END: "DEAD END",
184
+ FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
183
185
  }
184
186
 
185
187
 
@@ -241,6 +243,17 @@ class KindEvidence:
241
243
  example_summaries: list[str]
242
244
 
243
245
 
246
+ @dataclass
247
+ class ProjectPattern:
248
+ tool_name: str
249
+ failure_key: str
250
+ fix_key: str
251
+ session_count: int
252
+ avg_wasted_turns: float
253
+ total_waste_usd: float
254
+ example_sessions: list[str]
255
+
256
+
244
257
  @dataclass
245
258
  class AggregateReport:
246
259
  period_label: str # human-readable, e.g. "last 7 days" or "2026-05-01..2026-05-15"
@@ -250,6 +263,7 @@ class AggregateReport:
250
263
  waste_cost_usd: float
251
264
  by_kind: dict[FindingKind, KindEvidence]
252
265
  patches: list[Patch]
266
+ project_patterns: list[ProjectPattern] = field(default_factory=list)
253
267
 
254
268
 
255
269
  # ---------------------------------------------------------------------------
@@ -1,7 +1,8 @@
1
1
  """Patch generator — turns Findings into copy-pasteable CLAUDE.md diffs.
2
2
 
3
3
  generate(diagnosis) -> Diagnosis (single-session path)
4
- generate_from_evidence(evidence) -> list[Patch] (cross-session path)
4
+ generate_from_evidence(evidence) -> list[Patch] (cross-session path, generic findings)
5
+ generate_from_patterns(patterns) -> list[Patch] (cross-session path, project patterns)
5
6
  """
6
7
  from __future__ import annotations
7
8
 
@@ -11,7 +12,7 @@ from typing import TYPE_CHECKING
11
12
  from cctx.models import FindingKind, Patch
12
13
 
13
14
  if TYPE_CHECKING:
14
- from cctx.models import Diagnosis, Finding, KindEvidence
15
+ from cctx.models import Diagnosis, Finding, KindEvidence, ProjectPattern
15
16
 
16
17
  # ---------------------------------------------------------------------------
17
18
  # Patch templates (append-style unified diffs, v0)
@@ -129,3 +130,25 @@ def generate_from_evidence(
129
130
  evidence_summary=example,
130
131
  ))
131
132
  return patches
133
+
134
+
135
+ def generate_from_patterns(patterns: list[ProjectPattern]) -> list[Patch]:
136
+ """Generate CLAUDE.md patches from cross-session ProjectPatterns."""
137
+ patches = []
138
+ for p in patterns:
139
+ diff = (
140
+ f"+## Project-specific: {p.tool_name}({p.failure_key})\n"
141
+ f"+When `{p.failure_key}` fails, use `{p.fix_key}` instead.\n"
142
+ f"+Re-discovered in {p.session_count} sessions "
143
+ f"(~${p.total_waste_usd:.2f} wasted)."
144
+ )
145
+ patches.append(Patch(
146
+ target_file="CLAUDE.md",
147
+ description=f"Project-specific: {p.failure_key} → {p.fix_key}",
148
+ unified_diff=diff,
149
+ finding_kind=FindingKind.PROJECT_PATTERN,
150
+ evidence_summary=(
151
+ f"Seen in {p.session_count} sessions, ~${p.total_waste_usd:.2f} wasted"
152
+ ),
153
+ ))
154
+ return patches
@@ -112,22 +112,23 @@ def render_aggregate(report: AggregateReport, *, console: Console | None = None)
112
112
  f"Waste: ${report.waste_cost_usd:.2f}"
113
113
  )
114
114
 
115
- if not report.by_kind:
115
+ if not report.by_kind and not report.project_patterns:
116
116
  con.print("\nNo findings across sessions.")
117
117
  return
118
118
 
119
119
  # Summary table
120
- table = Table(title="Finding frequency")
121
- table.add_column("Pattern")
122
- table.add_column("Sessions", justify="right")
123
- table.add_column("Waste ($)", justify="right")
124
- for kind, ev in report.by_kind.items():
125
- table.add_row(
126
- _KIND_LABEL.get(kind, kind.value),
127
- str(ev.session_count),
128
- f"${ev.total_waste_usd:.2f}",
129
- )
130
- con.print(table)
120
+ if report.by_kind:
121
+ table = Table(title="Finding frequency")
122
+ table.add_column("Pattern")
123
+ table.add_column("Sessions", justify="right")
124
+ table.add_column("Waste ($)", justify="right")
125
+ for kind, ev in report.by_kind.items():
126
+ table.add_row(
127
+ _KIND_LABEL.get(kind, kind.value),
128
+ str(ev.session_count),
129
+ f"${ev.total_waste_usd:.2f}",
130
+ )
131
+ con.print(table)
131
132
 
132
133
  # Patches
133
134
  if report.patches:
@@ -137,6 +138,25 @@ def render_aggregate(report: AggregateReport, *, console: Console | None = None)
137
138
  syntax = Syntax(patch.unified_diff, "diff", theme="monokai", word_wrap=True)
138
139
  con.print(syntax)
139
140
 
141
+ # Project-specific patterns
142
+ if report.project_patterns:
143
+ con.print()
144
+ pp_table = Table(title="Project-specific patterns")
145
+ pp_table.add_column("Failure", style="bold")
146
+ pp_table.add_column("Fix")
147
+ pp_table.add_column("Sessions", justify="right", style="dim")
148
+ pp_table.add_column("Avg turns", justify="right", style="dim")
149
+ pp_table.add_column("Waste", justify="right")
150
+ for pp in report.project_patterns:
151
+ pp_table.add_row(
152
+ pp.failure_key,
153
+ pp.fix_key,
154
+ str(pp.session_count),
155
+ f"{pp.avg_wasted_turns:.1f}",
156
+ f"~${pp.total_waste_usd:.2f}",
157
+ )
158
+ con.print(pp_table)
159
+
140
160
 
141
161
  def render_aggregate_drilldown(
142
162
  diagnoses: list,