cctx-cli 1.14.0__tar.gz → 1.16.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 (194) hide show
  1. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/CHANGELOG.md +18 -0
  2. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/__init__.py +1 -1
  4. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/__init__.py +4 -0
  5. cctx_cli-1.16.0/cctx/diagnostician/patterns/compaction.py +114 -0
  6. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/dead_end.py +2 -1
  7. cctx_cli-1.16.0/cctx/diagnostician/patterns/exploration_thrash.py +192 -0
  8. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/stale_context.py +4 -2
  9. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/models.py +29 -23
  10. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/recommender/claude_md.py +27 -5
  11. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/pyproject.toml +1 -1
  12. cctx_cli-1.16.0/tests/test_compaction_classifier.py +373 -0
  13. cctx_cli-1.16.0/tests/test_exploration_thrash_classifier.py +272 -0
  14. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_harvest_emit.py +9 -7
  15. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.github/workflows/ci.yml +0 -0
  16. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.github/workflows/publish.yml +0 -0
  17. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.github/workflows/release.yml +0 -0
  18. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/.gitignore +0 -0
  19. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/CLAUDE.md +0 -0
  20. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/DESIGN.md +0 -0
  21. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/PRODUCT.md +0 -0
  22. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/README.md +0 -0
  23. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/action.yml +0 -0
  24. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/agents.py +0 -0
  25. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/cli.py +0 -0
  26. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/aggregate.py +0 -0
  27. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/inflection.py +0 -0
  28. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  29. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/cache_hygiene.py +0 -0
  30. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/fan_out.py +0 -0
  31. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
  32. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  33. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  34. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  35. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/discovery.py +0 -0
  36. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/__init__.py +0 -0
  37. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/csv.py +0 -0
  38. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/json.py +0 -0
  39. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/exporters/jsonl.py +0 -0
  40. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/harvest.py +0 -0
  41. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/hook_installer.py +0 -0
  42. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/parsers/__init__.py +0 -0
  43. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/parsers/claude_code.py +0 -0
  44. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/parsers/otel.py +0 -0
  45. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/pricing.py +0 -0
  46. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/recommender/__init__.py +0 -0
  47. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/recommender/evidence.py +0 -0
  48. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/__init__.py +0 -0
  49. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/github.py +0 -0
  50. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/report.py +0 -0
  51. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  52. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/terminal.py +0 -0
  53. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/renderers/trace_tui.py +0 -0
  54. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/tokenizer.py +0 -0
  55. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx/watcher.py +0 -0
  56. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/cctx-project-brief.md +0 -0
  57. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/demo.gif +0 -0
  58. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/demo.tape +0 -0
  59. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  60. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  61. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  62. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
  63. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/quickstart-otel.md +0 -0
  64. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  65. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  66. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  67. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
  68. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  69. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
  70. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/plans/2026-06-19-otel-parser.md +0 -0
  71. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  72. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  73. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  74. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  75. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  76. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
  77. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  78. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
  79. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
  80. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/docs/superpowers/specs/2026-06-19-otel-parser-design.md +0 -0
  81. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/__init__.py +0 -0
  82. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/conftest.py +0 -0
  83. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/__init__.py +0 -0
  84. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/conftest.py +0 -0
  85. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_dead_end.py +0 -0
  86. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_inflection.py +0 -0
  87. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_orchestrator.py +0 -0
  88. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_project_specific.py +0 -0
  89. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_retry_loop.py +0 -0
  90. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_scope_creep.py +0 -0
  91. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_stale_context.py +0 -0
  92. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  93. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/exporters/__init__.py +0 -0
  94. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/exporters/test_csv.py +0 -0
  95. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/exporters/test_jsonl.py +0 -0
  96. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/README.md +0 -0
  97. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  98. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  99. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  100. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  101. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  102. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  103. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  104. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  105. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  106. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  107. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  108. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  109. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  110. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  111. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  112. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  113. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  114. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  115. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  116. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  117. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  118. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  119. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  120. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  121. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  122. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  123. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  124. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  125. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  126. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  127. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  128. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  129. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  130. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  131. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  132. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  133. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  134. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  135. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  136. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  137. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  138. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  139. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  140. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  141. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  142. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  143. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  144. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  145. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  146. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  147. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  148. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  149. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  150. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  151. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  152. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  153. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  154. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  155. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/otel_fanout.jsonl +0 -0
  156. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/otel_handoff.jsonl +0 -0
  157. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/scrub.py +0 -0
  158. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  159. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  160. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  161. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  162. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  163. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/parsers/__init__.py +0 -0
  164. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/parsers/test_claude_code.py +0 -0
  165. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/parsers/test_claude_code_integration.py +0 -0
  166. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/recommender/__init__.py +0 -0
  167. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/recommender/test_claude_md.py +0 -0
  168. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/recommender/test_evidence.py +0 -0
  169. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/renderers/__init__.py +0 -0
  170. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/renderers/test_report.py +0 -0
  171. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  172. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_agents.py +0 -0
  173. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_aggregate.py +0 -0
  174. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_cache_hygiene_classifier.py +0 -0
  175. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_cli.py +0 -0
  176. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_cli_export.py +0 -0
  177. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_diagnostician_subagents.py +0 -0
  178. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_discovery.py +0 -0
  179. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_efficacy.py +0 -0
  180. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_fanout_classifier.py +0 -0
  181. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_github_summary.py +0 -0
  182. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_harvest.py +0 -0
  183. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_harvest_check.py +0 -0
  184. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_init.py +0 -0
  185. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_models.py +0 -0
  186. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_models_project_pattern.py +0 -0
  187. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_otel_parser.py +0 -0
  188. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_recommender.py +0 -0
  189. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_savings_framing.py +0 -0
  190. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_smoke.py +0 -0
  191. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_terminal_renderer.py +0 -0
  192. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_tokenizer.py +0 -0
  193. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_trace_tui.py +0 -0
  194. {cctx_cli-1.14.0 → cctx_cli-1.16.0}/tests/test_watcher.py +0 -0
@@ -2,6 +2,24 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.16.0 (2026-06-20)
6
+
7
+ ### Features
8
+
9
+ - Exploration thrash classifier — detect read-heavy circling without progress (#99)
10
+ ([#124](https://github.com/jacquardlabs/cctx/pull/124),
11
+ [`21f3374`](https://github.com/jacquardlabs/cctx/commit/21f3374bb0e2c60db0411213773ad9dffca7ec1f))
12
+
13
+
14
+ ## v1.15.0 (2026-06-20)
15
+
16
+ ### Features
17
+
18
+ - Compaction findings — surface compaction events and re-fetch waste (#93)
19
+ ([#123](https://github.com/jacquardlabs/cctx/pull/123),
20
+ [`cdab064`](https://github.com/jacquardlabs/cctx/commit/cdab0640b720e127549af63c22747555bd832b2c))
21
+
22
+
5
23
  ## v1.14.0 (2026-06-20)
6
24
 
7
25
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.14.0
3
+ Version: 1.16.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.14.0"
3
+ __version__ = "1.16.0"
@@ -16,7 +16,9 @@ from typing import TYPE_CHECKING
16
16
  from cctx.diagnostician import inflection
17
17
  from cctx.diagnostician.patterns import (
18
18
  cache_hygiene,
19
+ compaction,
19
20
  dead_end,
21
+ exploration_thrash,
20
22
  fan_out,
21
23
  retry_loop,
22
24
  scope_creep,
@@ -154,6 +156,8 @@ def run(trace: SessionTrace) -> Diagnosis:
154
156
  *dead_end.classify(trace),
155
157
  *fan_out.classify(trace),
156
158
  *cache_hygiene.classify(trace),
159
+ *compaction.classify(trace),
160
+ *exploration_thrash.classify(trace),
157
161
  ]
158
162
  findings.sort(key=lambda f: f.first_turn)
159
163
 
@@ -0,0 +1,114 @@
1
+ """Compaction-event classifier.
2
+
3
+ Detects context-window compaction events and surfaces them as first-class
4
+ findings. Also attributes re-fetch waste: files read before compaction that
5
+ are read again after (token cost of the re-read attributed to the compaction).
6
+
7
+ Exported helpers:
8
+ is_compaction_turn(turn) — canonical compaction predicate used by
9
+ stale_context.py and dead_end.py (replaces their local implementations).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from cctx.models import Confidence, Finding, FindingKind, Severity
16
+
17
+ if TYPE_CHECKING:
18
+ from cctx.models import SessionTrace, Turn
19
+
20
+
21
+ def is_compaction_turn(turn: Turn) -> bool:
22
+ """True if this turn represents a context-window compaction event."""
23
+ if turn.role == "system" and "compact" in turn.text.lower():
24
+ return True
25
+ return turn.text.startswith("<context_window")
26
+
27
+
28
+ def _classify_impl(trace: SessionTrace) -> list[Finding]:
29
+ compaction_turns = [t for t in trace.turns if is_compaction_turn(t)]
30
+ if not compaction_turns:
31
+ return []
32
+
33
+ first_compaction_turn = compaction_turns[0].turn_number
34
+
35
+ # Build map of files read before first compaction: key → token_count
36
+ pre_reads: dict[str, int] = {}
37
+ for turn in trace.turns:
38
+ if turn.turn_number >= first_compaction_turn:
39
+ break
40
+ for tu in turn.tool_uses:
41
+ if tu.tool_name == "Read":
42
+ fp = tu.tool_input.get("file_path", "")
43
+ if not fp:
44
+ continue
45
+ # Find matching tool result to get token count
46
+ for tr in turn.tool_results:
47
+ if tr.tool_use_id == tu.tool_use_id:
48
+ toks = (
49
+ tr.token_count
50
+ if tr.token_count > 0
51
+ else len(tr.content.split()) * 4 // 3
52
+ )
53
+ pre_reads[f"Read:{fp}"] = toks
54
+
55
+ # Detect re-fetches after compaction (first occurrence only per file)
56
+ re_fetches: list[dict] = []
57
+ for turn in trace.turns:
58
+ if turn.turn_number <= first_compaction_turn:
59
+ continue
60
+ for tu in turn.tool_uses:
61
+ if tu.tool_name == "Read":
62
+ fp = tu.tool_input.get("file_path", "")
63
+ key = f"Read:{fp}"
64
+ if key in pre_reads:
65
+ re_fetches.append({
66
+ "tool_name": tu.tool_name,
67
+ "path": fp,
68
+ "turn": turn.turn_number,
69
+ "tokens": pre_reads[key],
70
+ })
71
+ del pre_reads[key] # only flag first re-fetch per file
72
+
73
+ total_refetch_tokens = sum(r["tokens"] for r in re_fetches)
74
+ n_compactions = len(compaction_turns)
75
+ compaction_turn_numbers = [t.turn_number for t in compaction_turns]
76
+
77
+ severity = Severity.HIGH if re_fetches else Severity.LOW
78
+ confidence = Confidence.HIGH
79
+
80
+ parts = [
81
+ f"{n_compactions} compaction event{'s' if n_compactions > 1 else ''} "
82
+ f"(turn{'s' if n_compactions > 1 else ''} "
83
+ f"{', '.join(str(n) for n in compaction_turn_numbers)})"
84
+ ]
85
+ if re_fetches:
86
+ n_files = len(re_fetches)
87
+ parts.append(
88
+ f"{n_files} file{'s' if n_files > 1 else ''} re-fetched after compaction "
89
+ f"(~{total_refetch_tokens:,} tokens)"
90
+ )
91
+ summary = "; ".join(parts)
92
+
93
+ return [Finding(
94
+ kind=FindingKind.COMPACTION,
95
+ severity=severity,
96
+ confidence=confidence,
97
+ first_turn=compaction_turn_numbers[0],
98
+ last_turn=compaction_turn_numbers[-1],
99
+ evidence={
100
+ "n_compactions": n_compactions,
101
+ "compaction_turns": compaction_turn_numbers,
102
+ "re_fetches": re_fetches,
103
+ "total_refetch_tokens": total_refetch_tokens,
104
+ },
105
+ cost_usd=None,
106
+ summary=summary,
107
+ )]
108
+
109
+
110
+ def classify(trace: SessionTrace) -> list[Finding]:
111
+ try:
112
+ return _classify_impl(trace)
113
+ except Exception:
114
+ return []
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
  import json
21
21
  from typing import TYPE_CHECKING
22
22
 
23
+ from cctx.diagnostician.patterns.compaction import is_compaction_turn
23
24
  from cctx.models import Confidence, Finding, FindingKind, Severity
24
25
 
25
26
  if TYPE_CHECKING:
@@ -65,7 +66,7 @@ def _classify_impl(trace: SessionTrace) -> list[Finding]:
65
66
 
66
67
  for turn in trace.turns:
67
68
  # Compaction resets state
68
- if turn.text.startswith("<context_window"):
69
+ if is_compaction_turn(turn):
69
70
  run_tool = run_key = None
70
71
  run_count = 0
71
72
  continue
@@ -0,0 +1,192 @@
1
+ """Exploration-thrash classifier.
2
+
3
+ Detects when the assistant circles with repeated read/search tool calls
4
+ instead of making progress — high ratio of read-only tools to write/execute
5
+ tools in a window, or repeated identical discovery commands, with no file
6
+ edits or test runs in N consecutive turns.
7
+
8
+ Signals:
9
+ 1. Sliding window of WINDOW_SIZE consecutive assistant turns where ≥ 80%
10
+ of tool calls are read-only and no Write/Edit appears.
11
+ 2. Any (tool_name, key) read-only pair called ≥ REPEAT_THRESHOLD times
12
+ across the full session.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from collections import Counter
18
+ from typing import TYPE_CHECKING
19
+
20
+ from cctx.models import Confidence, Finding, FindingKind, Severity
21
+
22
+ if TYPE_CHECKING:
23
+ from cctx.models import SessionTrace
24
+
25
+ WINDOW_SIZE = 6
26
+ READ_RATIO_THRESHOLD = 0.80
27
+ REPEAT_THRESHOLD = 3
28
+
29
+ _READ_TOOLS = frozenset({"Read", "Grep", "Glob"})
30
+ _WRITE_TOOLS = frozenset({"Write", "Edit", "NotebookEdit"})
31
+
32
+ _READ_BASH_PREFIXES = (
33
+ "ls",
34
+ "find",
35
+ "cat",
36
+ "head",
37
+ "tail",
38
+ "grep",
39
+ "rg",
40
+ "ag",
41
+ "wc",
42
+ "file",
43
+ "stat",
44
+ "echo",
45
+ "pwd",
46
+ "which",
47
+ "type",
48
+ "less",
49
+ "git log",
50
+ "git diff",
51
+ "git show",
52
+ "git status",
53
+ "git blame",
54
+ )
55
+
56
+
57
+ def _is_read_only(tool_name: str, tool_input: dict) -> bool:
58
+ if tool_name in _READ_TOOLS:
59
+ return True
60
+ if tool_name in _WRITE_TOOLS:
61
+ return False
62
+ if tool_name == "Bash":
63
+ cmd = tool_input.get("command", "").lstrip()
64
+ return any(cmd.startswith(p) for p in _READ_BASH_PREFIXES)
65
+ return False
66
+
67
+
68
+ def _tool_key(tool_name: str, tool_input: dict) -> str:
69
+ """Canonical key for deduplication."""
70
+ match tool_name:
71
+ case "Bash":
72
+ return tool_input.get("command", "").strip()
73
+ case "Read" | "Write" | "Edit":
74
+ return tool_input.get("file_path", "")
75
+ case "Grep" | "Glob":
76
+ return tool_input.get("pattern", "")
77
+ case _:
78
+ return json.dumps(tool_input, sort_keys=True)
79
+
80
+
81
+ def _classify_impl(trace: SessionTrace) -> list[Finding]:
82
+ # Only look at assistant turns with tool calls
83
+ active_turns = [
84
+ t for t in trace.turns
85
+ if t.role == "assistant" and t.tool_uses
86
+ ]
87
+
88
+ thrash_windows: list[dict] = []
89
+
90
+ # Sliding window detection — requires at least WINDOW_SIZE active turns
91
+ for i in range(max(0, len(active_turns) - WINDOW_SIZE + 1)):
92
+ window = active_turns[i : i + WINDOW_SIZE]
93
+ all_calls = [
94
+ (tu.tool_name, tu.tool_input)
95
+ for t in window
96
+ for tu in t.tool_uses
97
+ ]
98
+ if not all_calls:
99
+ continue
100
+ read_count = sum(
101
+ 1 for name, inp in all_calls if _is_read_only(name, inp)
102
+ )
103
+ ratio = read_count / len(all_calls)
104
+ has_write = any(name in _WRITE_TOOLS for name, _ in all_calls)
105
+
106
+ if ratio >= READ_RATIO_THRESHOLD and not has_write:
107
+ # Avoid double-counting overlapping windows that cover the same turns
108
+ if (
109
+ thrash_windows
110
+ and thrash_windows[-1]["last_turn"] >= window[0].turn_number
111
+ ):
112
+ continue
113
+ thrash_windows.append({
114
+ "first_turn": window[0].turn_number,
115
+ "last_turn": window[-1].turn_number,
116
+ "read_ratio": round(ratio, 2),
117
+ "total_calls": len(all_calls),
118
+ })
119
+
120
+ # Repeated identical reads
121
+ read_call_counts: Counter[str] = Counter()
122
+ read_call_turns: dict[str, list[int]] = {}
123
+ for turn in trace.turns:
124
+ if turn.role != "assistant":
125
+ continue
126
+ for tu in turn.tool_uses:
127
+ if _is_read_only(tu.tool_name, tu.tool_input):
128
+ key = f"{tu.tool_name}:{_tool_key(tu.tool_name, tu.tool_input)}"
129
+ read_call_counts[key] += 1
130
+ read_call_turns.setdefault(key, []).append(turn.turn_number)
131
+
132
+ repeated_reads: list[dict] = []
133
+ for key, count in read_call_counts.items():
134
+ if count >= REPEAT_THRESHOLD:
135
+ tool_name, _, call_key = key.partition(":")
136
+ repeated_reads.append({
137
+ "tool_name": tool_name,
138
+ "key": call_key[:60],
139
+ "count": count,
140
+ "turns": read_call_turns[key],
141
+ })
142
+
143
+ if not thrash_windows and not repeated_reads:
144
+ return []
145
+
146
+ severity = Severity.HIGH if thrash_windows else Severity.MEDIUM
147
+ confidence = Confidence.MEDIUM
148
+
149
+ parts = []
150
+ if thrash_windows:
151
+ parts.append(
152
+ f"{len(thrash_windows)} exploration thrash window"
153
+ f"{'s' if len(thrash_windows) > 1 else ''} "
154
+ f"(turns {thrash_windows[0]['first_turn']}–{thrash_windows[-1]['last_turn']}, "
155
+ f"{thrash_windows[0]['read_ratio']:.0%} read-only)"
156
+ )
157
+ if repeated_reads:
158
+ worst = max(repeated_reads, key=lambda r: r["count"])
159
+ parts.append(
160
+ f"{worst['tool_name']}({worst['key']!r}) called {worst['count']}× identically"
161
+ )
162
+ summary = "; ".join(parts)
163
+
164
+ all_first = min(
165
+ [w["first_turn"] for w in thrash_windows]
166
+ + [r["turns"][0] for r in repeated_reads]
167
+ )
168
+ all_last = max(
169
+ [w["last_turn"] for w in thrash_windows]
170
+ + [r["turns"][-1] for r in repeated_reads]
171
+ )
172
+
173
+ return [Finding(
174
+ kind=FindingKind.EXPLORATION_THRASH,
175
+ severity=severity,
176
+ confidence=confidence,
177
+ first_turn=all_first,
178
+ last_turn=all_last,
179
+ evidence={
180
+ "thrash_windows": thrash_windows,
181
+ "repeated_reads": repeated_reads,
182
+ },
183
+ cost_usd=None,
184
+ summary=summary,
185
+ )]
186
+
187
+
188
+ def classify(trace: SessionTrace) -> list[Finding]:
189
+ try:
190
+ return _classify_impl(trace)
191
+ except Exception:
192
+ return []
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  from typing import TYPE_CHECKING
14
14
 
15
+ from cctx.diagnostician.patterns.compaction import is_compaction_turn
15
16
  from cctx.models import Confidence, Finding, FindingKind, Severity
16
17
 
17
18
  if TYPE_CHECKING:
@@ -33,8 +34,9 @@ def _make_3grams(text: str) -> set[tuple[str, ...]]:
33
34
  return {tuple(words[i : i + 3]) for i in range(len(words) - 2)}
34
35
 
35
36
 
37
+ # Backwards-compat alias — external callers that import _is_compaction keep working.
36
38
  def _is_compaction(turn: Turn) -> bool:
37
- return turn.role == "system" and "compact" in turn.text.lower()
39
+ return is_compaction_turn(turn)
38
40
 
39
41
 
40
42
  def _classify_impl(trace: SessionTrace) -> list[Finding]:
@@ -60,7 +62,7 @@ def _classify_impl(trace: SessionTrace) -> list[Finding]:
60
62
 
61
63
  # Find the turn number of any compaction events
62
64
  compaction_turns: set[int] = {
63
- t.turn_number for t in trace.turns if _is_compaction(t)
65
+ t.turn_number for t in trace.turns if is_compaction_turn(t)
64
66
  }
65
67
 
66
68
  last_turn_number = max((t.turn_number for t in trace.turns), default=0)
@@ -167,25 +167,29 @@ class SessionTrace:
167
167
 
168
168
 
169
169
  class FindingKind(str, Enum):
170
- RETRY_LOOP = "retry_loop"
171
- SCOPE_CREEP = "scope_creep"
172
- STALE_CONTEXT = "stale_context"
173
- TOOL_THRASH = "tool_thrash"
174
- DEAD_END = "dead_end"
175
- FANOUT_WASTE = "fanout_waste"
176
- PROJECT_PATTERN = "project_pattern"
177
- CACHE_HYGIENE = "cache_hygiene"
170
+ RETRY_LOOP = "retry_loop"
171
+ SCOPE_CREEP = "scope_creep"
172
+ STALE_CONTEXT = "stale_context"
173
+ TOOL_THRASH = "tool_thrash"
174
+ DEAD_END = "dead_end"
175
+ FANOUT_WASTE = "fanout_waste"
176
+ PROJECT_PATTERN = "project_pattern"
177
+ CACHE_HYGIENE = "cache_hygiene"
178
+ COMPACTION = "compaction"
179
+ EXPLORATION_THRASH = "exploration_thrash"
178
180
 
179
181
 
180
182
  KIND_LABEL: dict[FindingKind, str] = {
181
- FindingKind.RETRY_LOOP: "RETRY LOOP",
182
- FindingKind.SCOPE_CREEP: "SCOPE CREEP",
183
- FindingKind.STALE_CONTEXT: "STALE CONTEXT",
184
- FindingKind.TOOL_THRASH: "TOOL THRASH",
185
- FindingKind.DEAD_END: "DEAD END",
186
- FindingKind.FANOUT_WASTE: "FANOUT WASTE",
187
- FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
188
- FindingKind.CACHE_HYGIENE: "CACHE HYGIENE",
183
+ FindingKind.RETRY_LOOP: "RETRY LOOP",
184
+ FindingKind.SCOPE_CREEP: "SCOPE CREEP",
185
+ FindingKind.STALE_CONTEXT: "STALE CONTEXT",
186
+ FindingKind.TOOL_THRASH: "TOOL THRASH",
187
+ FindingKind.DEAD_END: "DEAD END",
188
+ FindingKind.FANOUT_WASTE: "FANOUT WASTE",
189
+ FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
190
+ FindingKind.CACHE_HYGIENE: "CACHE HYGIENE",
191
+ FindingKind.COMPACTION: "COMPACTION",
192
+ FindingKind.EXPLORATION_THRASH: "EXPLORATION THRASH",
189
193
  }
190
194
 
191
195
  # Maps FindingKind to the exact ## heading emitted by its recommender patch
@@ -193,13 +197,15 @@ KIND_LABEL: dict[FindingKind, str] = {
193
197
  # harvest.py imports this (never reaches into recommender/) so emit/sync can
194
198
  # identify cctx-managed sections without depending on the patch generator.
195
199
  MANAGED_HEADINGS: dict[FindingKind, str] = {
196
- FindingKind.RETRY_LOOP: "## Retry discipline",
197
- FindingKind.SCOPE_CREEP: "## Scope discipline",
198
- FindingKind.STALE_CONTEXT: "## Context hygiene",
199
- FindingKind.TOOL_THRASH: "## Tool-call discipline",
200
- FindingKind.DEAD_END: "## Exploration discipline",
201
- FindingKind.FANOUT_WASTE: "## Fan-out discipline",
202
- FindingKind.CACHE_HYGIENE: "## Cache hygiene",
200
+ FindingKind.RETRY_LOOP: "## Retry discipline",
201
+ FindingKind.SCOPE_CREEP: "## Scope discipline",
202
+ FindingKind.STALE_CONTEXT: "## Context hygiene",
203
+ FindingKind.TOOL_THRASH: "## Tool-call discipline",
204
+ FindingKind.DEAD_END: "## Exploration discipline",
205
+ FindingKind.FANOUT_WASTE: "## Fan-out discipline",
206
+ FindingKind.CACHE_HYGIENE: "## Cache hygiene",
207
+ FindingKind.COMPACTION: "## Compaction hygiene",
208
+ FindingKind.EXPLORATION_THRASH: "## Exploration thrash",
203
209
  }
204
210
 
205
211
  # Project-specific patterns use a heading that embeds tool+key, so the managed
@@ -76,15 +76,37 @@ _CACHE_HYGIENE_DIFF = """\
76
76
  +with a stable, cacheable preamble. A 10× cost difference separates a warm
77
77
  +cache hit from a cold input read."""
78
78
 
79
+ _COMPACTION_DIFF = """\
80
+ +## Compaction hygiene
81
+ +
82
+ +If context-window compaction occurs mid-session, assume all previously read files
83
+ +are gone from context. Re-read only files you actively need for the next step —
84
+ +don't reflexively reload everything. Better: compact earlier by summarizing large
85
+ +tool outputs once you've extracted what you need, so compaction doesn't erase
86
+ +work-in-progress state."""
87
+
88
+ _EXPLORATION_THRASH_DIFF = """\
89
+ +## Exploration thrash
90
+ +
91
+ +Before opening another file or running another search, state the specific
92
+ +question this call must answer and how the answer changes the next step. If
93
+ +you have read 6+ files without writing anything, stop and synthesise what
94
+ +you know. Never call the same read or grep more than twice with identical
95
+ +arguments — if the first two didn't help, a third won't either."""
96
+
79
97
  _TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
80
98
  # kind → (description, diff_body, target_file)
81
- FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
82
- FindingKind.SCOPE_CREEP: ("Add scope discipline rule", _SCOPE_CREEP_DIFF, "CLAUDE.md"),
99
+ FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
100
+ FindingKind.SCOPE_CREEP: ("Add scope discipline rule", _SCOPE_CREEP_DIFF, "CLAUDE.md"),
83
101
  FindingKind.STALE_CONTEXT: ("Add context hygiene rule", _STALE_CONTEXT_DIFF, "CLAUDE.md"),
84
- FindingKind.TOOL_THRASH: ("Add tool-call discipline rule", _TOOL_THRASH_DIFF, "CLAUDE.md"),
85
- FindingKind.DEAD_END: ("Add exploration discipline rule", _DEAD_END_DIFF, "CLAUDE.md"),
86
- FindingKind.FANOUT_WASTE: ("Add fan-out discipline rule", _FANOUT_WASTE_DIFF, "CLAUDE.md"),
102
+ FindingKind.TOOL_THRASH: ("Add tool-call discipline rule", _TOOL_THRASH_DIFF, "CLAUDE.md"),
103
+ FindingKind.DEAD_END: ("Add exploration discipline rule", _DEAD_END_DIFF, "CLAUDE.md"),
104
+ FindingKind.FANOUT_WASTE: ("Add fan-out discipline rule", _FANOUT_WASTE_DIFF, "CLAUDE.md"),
87
105
  FindingKind.CACHE_HYGIENE: ("Add cache hygiene rule", _CACHE_HYGIENE_DIFF, "CLAUDE.md"),
106
+ FindingKind.COMPACTION: ("Add compaction hygiene rule", _COMPACTION_DIFF, "CLAUDE.md"),
107
+ FindingKind.EXPLORATION_THRASH: (
108
+ "Add exploration thrash rule", _EXPLORATION_THRASH_DIFF, "CLAUDE.md"
109
+ ),
88
110
  }
89
111
 
90
112
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cctx-cli"
7
- version = "1.14.0"
7
+ version = "1.16.0"
8
8
  description = "Diagnose Claude Code sessions — find what went wrong, what it cost, and what to add to CLAUDE.md"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"