cctx-cli 1.7.0__tar.gz → 1.8.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 (177) hide show
  1. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/CHANGELOG.md +30 -0
  2. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/__init__.py +1 -1
  4. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/__init__.py +58 -3
  5. cctx_cli-1.8.0/cctx/diagnostician/patterns/fan_out.py +175 -0
  6. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/models.py +3 -0
  7. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/recommender/claude_md.py +10 -0
  8. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/pyproject.toml +1 -1
  9. cctx_cli-1.8.0/tests/test_fanout_classifier.py +344 -0
  10. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_harvest_emit.py +2 -1
  11. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.github/workflows/ci.yml +0 -0
  12. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.github/workflows/publish.yml +0 -0
  13. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.github/workflows/release.yml +0 -0
  14. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/.gitignore +0 -0
  15. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/CLAUDE.md +0 -0
  16. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/DESIGN.md +0 -0
  17. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/PRODUCT.md +0 -0
  18. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/README.md +0 -0
  19. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/action.yml +0 -0
  20. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/agents.py +0 -0
  21. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/cli.py +0 -0
  22. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/aggregate.py +0 -0
  23. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/inflection.py +0 -0
  24. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  25. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  26. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
  27. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  28. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  29. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  30. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  31. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/discovery.py +0 -0
  32. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/__init__.py +0 -0
  33. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/csv.py +0 -0
  34. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/json.py +0 -0
  35. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/exporters/jsonl.py +0 -0
  36. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/harvest.py +0 -0
  37. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/parsers/__init__.py +0 -0
  38. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/parsers/claude_code.py +0 -0
  39. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/pricing.py +0 -0
  40. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/recommender/__init__.py +0 -0
  41. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/recommender/evidence.py +0 -0
  42. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/__init__.py +0 -0
  43. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/github.py +0 -0
  44. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/report.py +0 -0
  45. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  46. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/terminal.py +0 -0
  47. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/renderers/trace_tui.py +0 -0
  48. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/tokenizer.py +0 -0
  49. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx/watcher.py +0 -0
  50. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/cctx-project-brief.md +0 -0
  51. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/demo.gif +0 -0
  52. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/demo.tape +0 -0
  53. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  54. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  55. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  56. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
  57. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  58. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  59. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  60. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
  61. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  62. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
  63. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  64. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  65. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  66. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  67. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  68. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
  69. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  70. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
  71. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
  72. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/__init__.py +0 -0
  73. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/conftest.py +0 -0
  74. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/__init__.py +0 -0
  75. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/conftest.py +0 -0
  76. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_dead_end.py +0 -0
  77. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_inflection.py +0 -0
  78. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_orchestrator.py +0 -0
  79. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_project_specific.py +0 -0
  80. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_retry_loop.py +0 -0
  81. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_scope_creep.py +0 -0
  82. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_stale_context.py +0 -0
  83. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  84. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/exporters/__init__.py +0 -0
  85. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/exporters/test_csv.py +0 -0
  86. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/exporters/test_jsonl.py +0 -0
  87. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/README.md +0 -0
  88. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  89. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  90. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  91. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  92. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  93. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  94. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  95. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  96. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  97. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  98. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  99. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  100. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  101. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  102. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  103. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  104. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  105. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  106. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  107. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  108. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  109. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  110. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  111. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  112. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  113. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  114. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  115. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  116. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  117. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  118. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  119. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  120. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  121. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  122. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  123. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  124. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  125. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  126. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  127. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  128. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  129. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  130. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  131. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  132. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  133. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  134. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  135. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  136. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  137. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  138. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  139. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  140. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  141. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  142. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  143. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  144. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  145. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  146. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/scrub.py +0 -0
  147. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  148. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  149. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  150. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  151. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  152. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/parsers/__init__.py +0 -0
  153. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/parsers/test_claude_code.py +0 -0
  154. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/parsers/test_claude_code_integration.py +0 -0
  155. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/recommender/__init__.py +0 -0
  156. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/recommender/test_claude_md.py +0 -0
  157. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/recommender/test_evidence.py +0 -0
  158. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/renderers/__init__.py +0 -0
  159. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/renderers/test_report.py +0 -0
  160. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  161. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_agents.py +0 -0
  162. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_aggregate.py +0 -0
  163. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_cli.py +0 -0
  164. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_cli_export.py +0 -0
  165. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_diagnostician_subagents.py +0 -0
  166. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_discovery.py +0 -0
  167. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_github_summary.py +0 -0
  168. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_harvest.py +0 -0
  169. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_harvest_check.py +0 -0
  170. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_models.py +0 -0
  171. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_models_project_pattern.py +0 -0
  172. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_recommender.py +0 -0
  173. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_smoke.py +0 -0
  174. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_terminal_renderer.py +0 -0
  175. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_tokenizer.py +0 -0
  176. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_trace_tui.py +0 -0
  177. {cctx_cli-1.7.0 → cctx_cli-1.8.0}/tests/test_watcher.py +0 -0
@@ -2,6 +2,36 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.8.0 (2026-06-11)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Add _FANOUT_WASTE_DIFF template so MANAGED_HEADINGS stays in sync
10
+ ([#110](https://github.com/jacquardlabs/cctx/pull/110),
11
+ [`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
12
+
13
+ - Ruff lint — move imports to top, break long lines in test file
14
+ ([#110](https://github.com/jacquardlabs/cctx/pull/110),
15
+ [`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
16
+
17
+ ### Features
18
+
19
+ - Add FindingKind.FANOUT_WASTE + KIND_LABEL + MANAGED_HEADINGS
20
+ ([#110](https://github.com/jacquardlabs/cctx/pull/110),
21
+ [`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
22
+
23
+ - Fan-out waste classifier (M16 #89) ([#110](https://github.com/jacquardlabs/cctx/pull/110),
24
+ [`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
25
+
26
+ - Fan_out classifier — Signal A (overlap) + Signal B (retry)
27
+ ([#110](https://github.com/jacquardlabs/cctx/pull/110),
28
+ [`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
29
+
30
+ - Wire fan_out classifier into diagnostician, add _patch_fanout_costs
31
+ ([#110](https://github.com/jacquardlabs/cctx/pull/110),
32
+ [`0123588`](https://github.com/jacquardlabs/cctx/commit/01235884bde2158307441c71f38d4cf96a2d8481))
33
+
34
+
5
35
  ## v1.7.0 (2026-06-11)
6
36
 
7
37
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.7.0
3
+ Version: 1.8.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.7.0"
3
+ __version__ = "1.8.0"
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING
16
16
  from cctx.diagnostician import inflection
17
17
  from cctx.diagnostician.patterns import (
18
18
  dead_end,
19
+ fan_out,
19
20
  retry_loop,
20
21
  scope_creep,
21
22
  stale_context,
@@ -41,6 +42,45 @@ def _patch_costs(findings: list[Finding], model: str | None) -> list[Finding]:
41
42
  return result
42
43
 
43
44
 
45
+ def _patch_fanout_costs(
46
+ findings: list[Finding],
47
+ subagent_costs: list[SubagentAttribution],
48
+ ) -> list[Finding]:
49
+ """Fill cost_usd on FANOUT_WASTE findings from subagent attribution data.
50
+
51
+ For overlap findings: picks the cheaper of the two subagents as waste.
52
+ For retry findings: attributes the full cost of the failed subagent.
53
+ Populates evidence['subagent_session_ids'] so run()'s dedup pass works.
54
+ """
55
+ cost_map = {a.session_id: a.total_cost_usd for a in subagent_costs}
56
+ result: list[Finding] = []
57
+ for f in findings:
58
+ if f.kind is FindingKind.FANOUT_WASTE:
59
+ signal = f.evidence.get("signal")
60
+ if signal == "overlap":
61
+ pair = [sid for sid in f.evidence.get("overlap_pair", []) if sid is not None]
62
+ if pair:
63
+ cheaper_cost, cheaper_sid = min(
64
+ (cost_map.get(sid, 0.0), sid) for sid in pair
65
+ )
66
+ f = dataclasses.replace(
67
+ f,
68
+ cost_usd=round(cheaper_cost, 4),
69
+ evidence={**f.evidence, "subagent_session_ids": [cheaper_sid]},
70
+ )
71
+ elif signal == "retry":
72
+ failed_sid = f.evidence.get("failed_session_id")
73
+ if failed_sid is not None:
74
+ cost = cost_map.get(failed_sid, 0.0)
75
+ f = dataclasses.replace(
76
+ f,
77
+ cost_usd=round(cost, 4),
78
+ evidence={**f.evidence, "subagent_session_ids": [failed_sid]},
79
+ )
80
+ result.append(f)
81
+ return result
82
+
83
+
44
84
  def _compute_own_cost(trace: SessionTrace, model: str | None) -> float:
45
85
  """Parent-turns-only cost — does not recurse into subagents.
46
86
 
@@ -111,17 +151,32 @@ def run(trace: SessionTrace) -> Diagnosis:
111
151
  *stale_context.classify(trace),
112
152
  *tool_thrash.classify(trace),
113
153
  *dead_end.classify(trace),
154
+ *fan_out.classify(trace),
114
155
  ]
115
156
  findings.sort(key=lambda f: f.first_turn)
116
157
 
117
158
  inflection_turn = inflection.detect(findings)
118
159
  findings = _patch_costs(findings, trace.primary_model)
119
160
 
161
+ # Fan-out cost patching requires attributions first.
162
+ subagent_costs = _collect_attributions(trace)
163
+ findings = _patch_fanout_costs(findings, subagent_costs)
164
+
120
165
  total_cost = round(_compute_inclusive_cost(trace), 4)
121
- waste_cost = sum(f.cost_usd for f in findings if f.cost_usd is not None)
122
- waste_cost = min(waste_cost, total_cost)
123
166
 
124
- subagent_costs = _collect_attributions(trace)
167
+ # Deduplicate fan-out waste: a subagent flagged by both overlap AND retry
168
+ # must not be double-counted. Collect unique wasted session IDs, sum once.
169
+ cost_map = {a.session_id: a.total_cost_usd for a in subagent_costs}
170
+ wasted_sids: set[str] = set()
171
+ for f in findings:
172
+ if f.kind is FindingKind.FANOUT_WASTE:
173
+ wasted_sids.update(f.evidence.get("subagent_session_ids", []))
174
+ fanout_waste = sum(cost_map.get(sid, 0.0) for sid in wasted_sids)
175
+ other_waste = sum(
176
+ f.cost_usd for f in findings
177
+ if f.cost_usd is not None and f.kind is not FindingKind.FANOUT_WASTE
178
+ )
179
+ waste_cost = min(other_waste + fanout_waste, total_cost)
125
180
 
126
181
  return Diagnosis(
127
182
  session_id=trace.session_id,
@@ -0,0 +1,175 @@
1
+ """Fan-out waste classifier.
2
+
3
+ classify(trace) -> list[Finding]
4
+
5
+ Signal A — OVERLAP: Two Agent calls with Jaccard >= 0.65 on word 3-grams,
6
+ both prompts >= 50 words.
7
+ Signal B — RETRY: Agent ToolResult is_error=True followed by the next Agent
8
+ call with Jaccard >= 0.50 on word 3-grams, both prompts >= 30 words.
9
+
10
+ Signal C (unused-result) is deferred — the 6-gram approach fires false
11
+ positives on paraphrased references and is not ship-ready.
12
+
13
+ cost_usd is set to None here; _patch_fanout_costs() in diagnostician/__init__.py
14
+ fills it in from SubagentAttribution data after run() collects attributions.
15
+ """
16
+ from __future__ import annotations
17
+
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, ToolUse
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Thresholds — documented here, not tuned at runtime
27
+ # ---------------------------------------------------------------------------
28
+
29
+ OVERLAP_JACCARD: float = 0.65 # minimum Jaccard on word 3-grams for overlap
30
+ OVERLAP_MIN_WORDS: int = 50 # both prompts must be this long
31
+
32
+ RETRY_JACCARD: float = 0.50 # minimum Jaccard for failed-retry detection
33
+ RETRY_MIN_WORDS: int = 30 # both prompts must be this long
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # N-gram helpers
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def _word_ngrams(text: str, n: int) -> set[tuple[str, ...]]:
41
+ words = text.lower().split()
42
+ if len(words) < n:
43
+ return set()
44
+ return {tuple(words[i : i + n]) for i in range(len(words) - n + 1)}
45
+
46
+
47
+ def _jaccard(a: set, b: set) -> float:
48
+ if not a or not b:
49
+ return 0.0
50
+ return len(a & b) / len(a | b)
51
+
52
+
53
+ def _get_prompt(tu: ToolUse) -> str:
54
+ return tu.tool_input.get("prompt") or tu.tool_input.get("description") or ""
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Signal A — Overlapping subagent prompts
59
+ # ---------------------------------------------------------------------------
60
+
61
+ def _signal_overlap(agent_calls: list[tuple[int, ToolUse]]) -> list[Finding]:
62
+ findings: list[Finding] = []
63
+ for i in range(len(agent_calls)):
64
+ turn_i, tu_i = agent_calls[i]
65
+ p_i = _get_prompt(tu_i)
66
+ words_i = p_i.split()
67
+ if len(words_i) < OVERLAP_MIN_WORDS:
68
+ continue
69
+ ng_i = _word_ngrams(p_i, 3)
70
+ for j in range(i + 1, len(agent_calls)):
71
+ turn_j, tu_j = agent_calls[j]
72
+ p_j = _get_prompt(tu_j)
73
+ words_j = p_j.split()
74
+ if len(words_j) < OVERLAP_MIN_WORDS:
75
+ continue
76
+ ng_j = _word_ngrams(p_j, 3)
77
+ score = _jaccard(ng_i, ng_j)
78
+ if score < OVERLAP_JACCARD:
79
+ continue
80
+ findings.append(Finding(
81
+ kind=FindingKind.FANOUT_WASTE,
82
+ severity=Severity.MEDIUM,
83
+ confidence=Confidence.MEDIUM,
84
+ first_turn=min(turn_i, turn_j),
85
+ last_turn=max(turn_i, turn_j),
86
+ evidence={
87
+ "signal": "overlap",
88
+ "overlap_pair": [tu_i.subagent_session_id, tu_j.subagent_session_id],
89
+ "jaccard": round(score, 3),
90
+ "prompt_a": p_i[:80],
91
+ "prompt_b": p_j[:80],
92
+ "subagent_session_ids": [], # filled by _patch_fanout_costs
93
+ },
94
+ cost_usd=None,
95
+ summary=f"Overlapping subagent prompts (Jaccard {score:.2f})",
96
+ ))
97
+ return findings
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Signal B — Failed subagent re-spawned with similar prompt
102
+ # ---------------------------------------------------------------------------
103
+
104
+ def _signal_retry(
105
+ agent_calls: list[tuple[int, ToolUse]],
106
+ result_map: dict[str, tuple[bool, str]], # tool_use_id -> (is_error, content)
107
+ ) -> list[Finding]:
108
+ findings: list[Finding] = []
109
+ for k, (turn_k, tu_k) in enumerate(agent_calls):
110
+ is_error, _content = result_map.get(tu_k.tool_use_id, (False, ""))
111
+ if not is_error:
112
+ continue
113
+ # Only check the immediate next Agent call (by list order = turn order)
114
+ if k + 1 >= len(agent_calls):
115
+ continue
116
+ turn_next, tu_next = agent_calls[k + 1]
117
+ p_failed = _get_prompt(tu_k)
118
+ p_retry = _get_prompt(tu_next)
119
+ if len(p_failed.split()) < RETRY_MIN_WORDS or len(p_retry.split()) < RETRY_MIN_WORDS:
120
+ continue
121
+ score = _jaccard(_word_ngrams(p_failed, 3), _word_ngrams(p_retry, 3))
122
+ if score < RETRY_JACCARD:
123
+ continue
124
+ findings.append(Finding(
125
+ kind=FindingKind.FANOUT_WASTE,
126
+ severity=Severity.HIGH,
127
+ confidence=Confidence.HIGH,
128
+ first_turn=turn_k,
129
+ last_turn=turn_next,
130
+ evidence={
131
+ "signal": "retry",
132
+ "failed_session_id": tu_k.subagent_session_id,
133
+ "jaccard": round(score, 3),
134
+ "failed_prompt": p_failed[:80],
135
+ "retry_prompt": p_retry[:80],
136
+ "subagent_session_ids": [], # filled by _patch_fanout_costs
137
+ },
138
+ cost_usd=None,
139
+ summary=f"Failed subagent re-spawned with similar prompt (Jaccard {score:.2f})",
140
+ ))
141
+ return findings
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Public entry point
146
+ # ---------------------------------------------------------------------------
147
+
148
+ def _classify_impl(trace: SessionTrace) -> list[Finding]:
149
+ # Collect Agent ToolUse in turn order
150
+ agent_calls: list[tuple[int, ToolUse]] = []
151
+ result_map: dict[str, tuple[bool, str]] = {}
152
+
153
+ for turn in trace.turns:
154
+ for tu in turn.tool_uses:
155
+ if tu.tool_name == "Agent":
156
+ agent_calls.append((turn.turn_number, tu))
157
+ for tr in turn.tool_results:
158
+ if tr.tool_name == "Agent":
159
+ result_map[tr.tool_use_id] = (tr.is_error, tr.content)
160
+
161
+ if len(agent_calls) < 2:
162
+ return []
163
+
164
+ findings: list[Finding] = [
165
+ *_signal_overlap(agent_calls),
166
+ *_signal_retry(agent_calls, result_map),
167
+ ]
168
+ return findings
169
+
170
+
171
+ def classify(trace: SessionTrace) -> list[Finding]:
172
+ try:
173
+ return _classify_impl(trace)
174
+ except Exception:
175
+ return []
@@ -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
+ FANOUT_WASTE = "fanout_waste"
175
176
  PROJECT_PATTERN = "project_pattern"
176
177
 
177
178
 
@@ -181,6 +182,7 @@ KIND_LABEL: dict[FindingKind, str] = {
181
182
  FindingKind.STALE_CONTEXT: "STALE CONTEXT",
182
183
  FindingKind.TOOL_THRASH: "TOOL THRASH",
183
184
  FindingKind.DEAD_END: "DEAD END",
185
+ FindingKind.FANOUT_WASTE: "FANOUT WASTE",
184
186
  FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
185
187
  }
186
188
 
@@ -194,6 +196,7 @@ MANAGED_HEADINGS: dict[FindingKind, str] = {
194
196
  FindingKind.STALE_CONTEXT: "## Context hygiene",
195
197
  FindingKind.TOOL_THRASH: "## Tool-call discipline",
196
198
  FindingKind.DEAD_END: "## Exploration discipline",
199
+ FindingKind.FANOUT_WASTE: "## Fan-out discipline",
197
200
  }
198
201
 
199
202
  # Project-specific patterns use a heading that embeds tool+key, so the managed
@@ -57,6 +57,15 @@ _DEAD_END_DIFF = """\
57
57
  +approaches already ruled out, and pick a meaningfully different one. Sunk effort on a
58
58
  +failing approach is not a reason to continue it."""
59
59
 
60
+ _FANOUT_WASTE_DIFF = """\
61
+ +## Fan-out discipline
62
+ +
63
+ +Before spawning multiple subagents in parallel, state what each one will return
64
+ +and verify the tasks don't overlap. After each subagent completes, confirm its
65
+ +result is actually consumed by the parent before spawning retries. Retry only
66
+ +after changing something meaningful about the task — identical re-spawns waste
67
+ +the full subagent cost with no new information."""
68
+
60
69
  _TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
61
70
  # kind → (description, diff_body, target_file)
62
71
  FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
@@ -64,6 +73,7 @@ _TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
64
73
  FindingKind.STALE_CONTEXT: ("Add context hygiene rule", _STALE_CONTEXT_DIFF, "CLAUDE.md"),
65
74
  FindingKind.TOOL_THRASH: ("Add tool-call discipline rule", _TOOL_THRASH_DIFF, "CLAUDE.md"),
66
75
  FindingKind.DEAD_END: ("Add exploration discipline rule", _DEAD_END_DIFF, "CLAUDE.md"),
76
+ FindingKind.FANOUT_WASTE: ("Add fan-out discipline rule", _FANOUT_WASTE_DIFF, "CLAUDE.md"),
67
77
  }
68
78
 
69
79
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cctx-cli"
7
- version = "1.7.0"
7
+ version = "1.8.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"