cctx-cli 1.0.0__tar.gz → 1.1.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 (158) hide show
  1. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/.github/workflows/publish.yml +2 -14
  2. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/CHANGELOG.md +24 -0
  3. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/PKG-INFO +1 -1
  4. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/__init__.py +1 -1
  5. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/cli.py +30 -3
  6. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/models.py +7 -0
  7. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/renderers/terminal.py +54 -1
  8. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/pyproject.toml +2 -1
  9. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_cli.py +80 -0
  10. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_models.py +54 -0
  11. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_terminal_renderer.py +100 -0
  12. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/.github/workflows/ci.yml +0 -0
  13. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/.github/workflows/release.yml +0 -0
  14. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/.gitignore +0 -0
  15. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/CLAUDE.md +0 -0
  16. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/DESIGN.md +0 -0
  17. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/PRODUCT.md +0 -0
  18. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/README.md +0 -0
  19. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/action.yml +0 -0
  20. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/__init__.py +0 -0
  21. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/aggregate.py +0 -0
  22. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/inflection.py +0 -0
  23. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  24. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  25. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  26. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  27. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  28. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  29. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/discovery.py +0 -0
  30. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/exporters/__init__.py +0 -0
  31. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/exporters/csv.py +0 -0
  32. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/exporters/jsonl.py +0 -0
  33. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/harvest.py +0 -0
  34. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/parsers/__init__.py +0 -0
  35. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/parsers/claude_code.py +0 -0
  36. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/pricing.py +0 -0
  37. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/recommender/__init__.py +0 -0
  38. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/recommender/claude_md.py +0 -0
  39. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/recommender/evidence.py +0 -0
  40. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/renderers/__init__.py +0 -0
  41. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/renderers/github.py +0 -0
  42. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/renderers/report.py +0 -0
  43. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  44. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/renderers/trace_tui.py +0 -0
  45. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/tokenizer.py +0 -0
  46. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx/watcher.py +0 -0
  47. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/cctx-project-brief.md +0 -0
  48. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/demo.gif +0 -0
  49. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/demo.tape +0 -0
  50. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  51. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  52. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  53. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  54. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  55. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  56. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  57. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  58. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  59. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  60. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  61. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/__init__.py +0 -0
  62. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/conftest.py +0 -0
  63. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/__init__.py +0 -0
  64. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/conftest.py +0 -0
  65. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/test_dead_end.py +0 -0
  66. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/test_inflection.py +0 -0
  67. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/test_orchestrator.py +0 -0
  68. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/test_retry_loop.py +0 -0
  69. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/test_scope_creep.py +0 -0
  70. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/test_stale_context.py +0 -0
  71. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  72. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/exporters/__init__.py +0 -0
  73. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/exporters/test_csv.py +0 -0
  74. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/exporters/test_jsonl.py +0 -0
  75. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/README.md +0 -0
  76. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  77. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  78. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  79. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  80. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  81. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  82. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  83. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  84. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  85. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  86. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  87. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  88. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  89. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  90. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  91. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  92. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  93. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  94. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  95. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  96. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  97. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  98. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  99. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  100. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  101. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  102. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  103. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  104. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  105. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  106. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  107. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  108. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  109. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  110. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  111. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  112. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  113. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  114. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  115. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  116. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  117. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  118. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  119. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  120. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  121. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  122. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  123. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  124. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  125. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  126. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  127. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  128. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  129. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  130. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  131. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  132. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  133. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  134. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/scrub.py +0 -0
  135. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  136. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  137. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  138. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  139. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  140. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/parsers/__init__.py +0 -0
  141. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/parsers/test_claude_code.py +0 -0
  142. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/parsers/test_claude_code_integration.py +0 -0
  143. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/recommender/__init__.py +0 -0
  144. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/recommender/test_claude_md.py +0 -0
  145. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/recommender/test_evidence.py +0 -0
  146. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/renderers/__init__.py +0 -0
  147. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/renderers/test_report.py +0 -0
  148. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  149. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_aggregate.py +0 -0
  150. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_cli_export.py +0 -0
  151. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_discovery.py +0 -0
  152. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_github_summary.py +0 -0
  153. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_harvest.py +0 -0
  154. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_harvest_check.py +0 -0
  155. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_smoke.py +0 -0
  156. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_tokenizer.py +0 -0
  157. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_trace_tui.py +0 -0
  158. {cctx_cli-1.0.0 → cctx_cli-1.1.0}/tests/test_watcher.py +0 -0
@@ -46,18 +46,6 @@ jobs:
46
46
 
47
47
  - name: Publish
48
48
  uses: pypa/gh-action-pypi-publish@release/v1
49
+ with:
50
+ skip-existing: true
49
51
 
50
- tag-action:
51
- name: Tag floating action version
52
- needs: [publish]
53
- runs-on: ubuntu-latest
54
- permissions:
55
- contents: write
56
- steps:
57
- - uses: actions/checkout@v4
58
-
59
- - name: Push floating major tag
60
- run: |
61
- MAJOR=$(echo "${{ github.event.release.tag_name }}" | cut -d. -f1)
62
- git tag -f "$MAJOR"
63
- git push origin "$MAJOR" --force
@@ -2,6 +2,30 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.1.0 (2026-05-17)
6
+
7
+ ### Chores
8
+
9
+ - Add skip-existing to pypi publish action
10
+ ([`23d7e16`](https://github.com/jacquardlabs/cctx/commit/23d7e16e18074da3c25899ba98298100ad3c1ad3))
11
+
12
+ ### Features
13
+
14
+ - M9 polish — verdict headline, --top N, --turn N
15
+ ([#83](https://github.com/jacquardlabs/cctx/pull/83),
16
+ [`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
17
+
18
+ - M9 polish — verdict headline, --top N, and --turn N
19
+ ([#83](https://github.com/jacquardlabs/cctx/pull/83),
20
+ [`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
21
+
22
+ ### Refactoring
23
+
24
+ - Cache verdict, fix markup=False bug, use reverse=True
25
+ ([#83](https://github.com/jacquardlabs/cctx/pull/83),
26
+ [`b0d2f27`](https://github.com/jacquardlabs/cctx/commit/b0d2f273a373c5a2f52c9de3a3fb2721da59c4f5))
27
+
28
+
5
29
  ## v1.0.0 (2026-05-17)
6
30
 
7
31
  ### Continuous Integration
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.0.0
3
+ Version: 1.1.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.0.0"
3
+ __version__ = "1.1.0"
@@ -31,6 +31,7 @@ from cctx.renderers.terminal import (
31
31
  render_harvest_results,
32
32
  render_projects,
33
33
  render_sessions,
34
+ render_turn,
34
35
  )
35
36
  from cctx.tokenizer import tokenize_session
36
37
 
@@ -245,6 +246,22 @@ def ls(project: Path | None) -> None:
245
246
  default=False,
246
247
  help="Exit 1 if any findings are detected (single-session only).",
247
248
  )
249
+ @click.option(
250
+ "--top",
251
+ "top_n",
252
+ default=None,
253
+ metavar="N",
254
+ type=click.IntRange(min=1),
255
+ help="Show only the top N patterns by session count (--since mode only).",
256
+ )
257
+ @click.option(
258
+ "--turn",
259
+ "turn_num",
260
+ default=None,
261
+ metavar="N",
262
+ type=click.IntRange(min=1),
263
+ help="Show details for turn N (single-session only).",
264
+ )
248
265
  def autopsy(
249
266
  target: Path | None,
250
267
  since: str | None,
@@ -252,6 +269,8 @@ def autopsy(
252
269
  html_out: Path | None,
253
270
  github_summary: bool,
254
271
  fail_on_findings: bool,
272
+ top_n: int | None,
273
+ turn_num: int | None,
255
274
  ) -> None:
256
275
  """Diagnose a session or project directory.
257
276
 
@@ -267,6 +286,10 @@ def autopsy(
267
286
  raise click.UsageError("--latest and --since are mutually exclusive.")
268
287
  if fail_on_findings and since is not None:
269
288
  raise click.UsageError("--fail-on-findings is not supported with --since.")
289
+ if top_n is not None and since is None:
290
+ raise click.UsageError("--top requires --since.")
291
+ if turn_num is not None and since is not None:
292
+ raise click.UsageError("--turn is not supported with --since.")
270
293
 
271
294
  if target is None:
272
295
  if not latest:
@@ -305,6 +328,8 @@ def autopsy(
305
328
  start, end, label = parse_since(since)
306
329
  diagnoses = aggregate.run(project_dir, start, end)
307
330
  ev = evidence_mod.accumulate(diagnoses)
331
+ if top_n is not None:
332
+ ev = dict(sorted(ev.items(), key=lambda x: x[1].session_count, reverse=True)[:top_n])
308
333
  patches = claude_md.generate_from_evidence(ev)
309
334
  report = AggregateReport(
310
335
  period_label=label,
@@ -327,14 +352,16 @@ def autopsy(
327
352
  trace = tokenize_session(parse_session(target))
328
353
  diagnosis = diagnostician.run(trace)
329
354
  diagnosis = claude_md.generate(diagnosis)
330
- if html_out is not None:
355
+ if turn_num is not None:
356
+ render_turn(trace, diagnosis, turn_num)
357
+ elif html_out is not None:
331
358
  from cctx.renderers.report import render_html
332
359
  html_out.write_text(render_html(diagnosis, trace), encoding="utf-8")
333
360
  click.echo(f"HTML report written to {html_out}")
334
- if github_summary:
361
+ elif github_summary:
335
362
  from cctx.renderers.github import write_github_summary
336
363
  write_github_summary(diagnosis)
337
- if html_out is None and not github_summary:
364
+ else:
338
365
  render_diagnosis(diagnosis, session_path=target)
339
366
  if fail_on_findings and diagnosis.findings:
340
367
  raise SystemExit(1)
@@ -225,6 +225,13 @@ class Diagnosis:
225
225
  waste_cost_usd: float
226
226
  analysed_at: datetime
227
227
 
228
+ @property
229
+ def verdict(self) -> str:
230
+ if not self.findings:
231
+ return "clean session"
232
+ seen = dict.fromkeys(f.kind for f in self.findings)
233
+ return " + ".join(KIND_LABEL[k] for k in seen)
234
+
228
235
 
229
236
  @dataclass
230
237
  class KindEvidence:
@@ -24,7 +24,7 @@ from cctx.models import KIND_LABEL, FindingKind, Severity
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from cctx.discovery import ProjectInfo
27
- from cctx.models import AggregateReport, Diagnosis
27
+ from cctx.models import AggregateReport, Diagnosis, SessionTrace
28
28
 
29
29
  _SEVERITY_STYLE = {
30
30
  Severity.HIGH: "bold red",
@@ -49,6 +49,9 @@ def render_diagnosis(
49
49
 
50
50
  # Header
51
51
  con.print(Rule(f"cctx autopsy — session {diagnosis.session_id}"))
52
+ verdict = diagnosis.verdict
53
+ verdict_style = "bold green" if not diagnosis.findings else "bold red"
54
+ con.print(Text(f"Verdict: {verdict}", style=verdict_style))
52
55
  cost_line = f"Session cost: ~${diagnosis.total_cost_usd:.2f}"
53
56
  if diagnosis.waste_cost_usd > 0:
54
57
  pct = (
@@ -169,6 +172,56 @@ def render_aggregate_drilldown(
169
172
  con.print("No matching findings.")
170
173
 
171
174
 
175
+ def render_turn(
176
+ trace: SessionTrace,
177
+ diagnosis: Diagnosis,
178
+ turn_num: int,
179
+ *,
180
+ console: Console | None = None,
181
+ ) -> None:
182
+ """Render details for a single turn N from a session."""
183
+ con = console or _default_console()
184
+
185
+ turn = next((t for t in trace.turns if t.turn_number == turn_num), None)
186
+ if turn is None:
187
+ con.print(Text(
188
+ f"Turn {turn_num} not found (session has {len(trace.turns)} turns).",
189
+ style="red",
190
+ ))
191
+ return
192
+
193
+ con.print(Rule(f"Turn {turn_num} — {turn.role} — {turn.timestamp.strftime('%H:%M:%S')}"))
194
+
195
+ text = turn.text
196
+ if text:
197
+ preview = text[:500]
198
+ if len(text) > 500:
199
+ preview += f"\n… [{len(text) - 500} more chars]"
200
+ con.print(preview)
201
+
202
+ for tu in turn.tool_uses:
203
+ con.print(Text(f" tool_use: {tu.tool_name}", style="cyan"))
204
+
205
+ for tr in turn.tool_results:
206
+ style = "red" if tr.is_error else "dim"
207
+ content = tr.content
208
+ preview = content[:200] + ("…" if len(content) > 200 else "")
209
+ con.print(Text(f" tool_result ({tr.tool_name}): {preview}", style=style))
210
+
211
+ # Findings that span this turn
212
+ touching = [
213
+ f for f in diagnosis.findings
214
+ if f.first_turn <= turn_num <= (f.last_turn or f.first_turn)
215
+ ]
216
+ if touching:
217
+ con.print()
218
+ con.print(Text("Findings active at this turn:", style="bold"))
219
+ for finding in touching:
220
+ style = _SEVERITY_STYLE.get(finding.severity, "")
221
+ label = _KIND_LABEL.get(finding.kind, finding.kind.value.upper())
222
+ con.print(Text(f" [{label}]", style=style), finding.summary)
223
+
224
+
172
225
  def render_harvest_results(
173
226
  results: list, # list[ApplyResult] — ApplyStatus is harvest-internal; imported lazily
174
227
  *,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cctx-cli"
7
- version = "1.0.0"
7
+ version = "1.1.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"
@@ -47,6 +47,7 @@ version_toml = ["pyproject.toml:project.version"]
47
47
  version_variables = ["cctx/__init__.py:__version__"]
48
48
  tag_format = "v{version}"
49
49
  commit_message = "chore(release): v{version} [skip ci]"
50
+ major_on_zero = false
50
51
  build_command = "pip install build && python -m build"
51
52
  upload_to_vcs_release = true
52
53
 
@@ -309,3 +309,83 @@ def test_fail_on_findings_incompatible_with_since(runner):
309
309
  ["autopsy", str(FIXTURE_PATH.parent), "--since", "7", "--fail-on-findings"],
310
310
  )
311
311
  assert result.exit_code != 0
312
+
313
+
314
+ # ---------------------------------------------------------------------------
315
+ # --top N tests (#75)
316
+ # ---------------------------------------------------------------------------
317
+
318
+
319
+ def test_top_requires_since(runner, session_jsonl):
320
+ """--top without --since → non-zero exit (UsageError)."""
321
+ from cctx.cli import cli
322
+
323
+ result = runner.invoke(cli, ["autopsy", str(session_jsonl), "--top", "3"])
324
+ assert result.exit_code != 0
325
+ assert "since" in result.output.lower() or "Error" in result.output
326
+
327
+
328
+ def test_top_with_since_accepted(runner, tmp_path):
329
+ """--top N with --since → exit 0."""
330
+ from cctx.cli import cli
331
+
332
+ project_dir = tmp_path / "-Users-test-Projects-demo"
333
+ project_dir.mkdir()
334
+ session_id = "top-test-sess"
335
+ line = {
336
+ "type": "user", "uuid": f"{session_id}-u1", "parentUuid": None,
337
+ "isSidechain": False, "timestamp": "2026-05-14T10:00:00.000Z",
338
+ "sessionId": session_id, "version": "2.1.138",
339
+ "cwd": "/Users/test/Projects/demo", "gitBranch": "main",
340
+ "userType": "external", "entrypoint": "cli",
341
+ "message": {"role": "user", "content": "hello"},
342
+ }
343
+ (project_dir / f"{session_id}.jsonl").write_text(json.dumps(line) + "\n")
344
+
345
+ result = runner.invoke(
346
+ cli, ["autopsy", str(project_dir), "--since", "7", "--top", "2"],
347
+ catch_exceptions=False,
348
+ )
349
+ assert result.exit_code == 0
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # --turn N tests (#76)
354
+ # ---------------------------------------------------------------------------
355
+
356
+
357
+ def test_turn_incompatible_with_since(runner, tmp_path):
358
+ """--turn N + --since → non-zero exit (UsageError)."""
359
+ from cctx.cli import cli
360
+
361
+ project_dir = tmp_path / "-Users-test-Projects-demo"
362
+ project_dir.mkdir()
363
+ result = runner.invoke(
364
+ cli, ["autopsy", str(project_dir), "--since", "7", "--turn", "3"],
365
+ )
366
+ assert result.exit_code != 0
367
+ assert "since" in result.output.lower() or "Error" in result.output
368
+
369
+
370
+ def test_turn_shows_turn_details(runner, session_jsonl):
371
+ """--turn 1 on a session with one turn prints turn details."""
372
+ from cctx.cli import cli
373
+
374
+ result = runner.invoke(
375
+ cli, ["autopsy", str(session_jsonl), "--turn", "1"],
376
+ catch_exceptions=False,
377
+ )
378
+ assert result.exit_code == 0
379
+ assert "Turn 1" in result.output
380
+
381
+
382
+ def test_turn_out_of_range_shows_not_found(runner, session_jsonl):
383
+ """--turn 999 on a one-turn session shows 'not found' message."""
384
+ from cctx.cli import cli
385
+
386
+ result = runner.invoke(
387
+ cli, ["autopsy", str(session_jsonl), "--turn", "999"],
388
+ catch_exceptions=False,
389
+ )
390
+ assert result.exit_code == 0
391
+ assert "not found" in result.output.lower() or "999" in result.output
@@ -643,3 +643,57 @@ def test_aggregate_report_instantiates():
643
643
  )
644
644
  assert report.sessions_analysed == 12
645
645
  assert FindingKind.RETRY_LOOP in report.by_kind
646
+
647
+
648
+ # ---------------------------------------------------------------------------
649
+ # Diagnosis.verdict property (#74)
650
+ # ---------------------------------------------------------------------------
651
+
652
+
653
+ def _make_finding_of_kind(kind, first_turn=5):
654
+ from cctx.models import Confidence, Finding, Severity
655
+ return Finding(
656
+ kind=kind,
657
+ severity=Severity.MEDIUM,
658
+ confidence=Confidence.MEDIUM,
659
+ first_turn=first_turn,
660
+ last_turn=None,
661
+ evidence={},
662
+ cost_usd=None,
663
+ summary="test",
664
+ )
665
+
666
+
667
+ def _make_diag(findings):
668
+ from cctx.models import Diagnosis
669
+ return Diagnosis(
670
+ session_id="abc",
671
+ findings=findings,
672
+ inflection_turn=None,
673
+ patches=[],
674
+ total_cost_usd=1.0,
675
+ waste_cost_usd=0.0,
676
+ analysed_at=_utcnow(),
677
+ )
678
+
679
+
680
+ def test_verdict_clean_session():
681
+ d = _make_diag([])
682
+ assert d.verdict == "clean session"
683
+
684
+
685
+ def test_verdict_single_finding():
686
+ from cctx.models import FindingKind
687
+ d = _make_diag([_make_finding_of_kind(FindingKind.RETRY_LOOP)])
688
+ assert d.verdict == "RETRY LOOP"
689
+
690
+
691
+ def test_verdict_multiple_kinds_deduped_in_order():
692
+ from cctx.models import FindingKind
693
+ findings = [
694
+ _make_finding_of_kind(FindingKind.SCOPE_CREEP),
695
+ _make_finding_of_kind(FindingKind.RETRY_LOOP),
696
+ _make_finding_of_kind(FindingKind.SCOPE_CREEP), # duplicate kind
697
+ ]
698
+ d = _make_diag(findings)
699
+ assert d.verdict == "SCOPE CREEP + RETRY LOOP"
@@ -109,3 +109,103 @@ def test_patch_diff_shown():
109
109
  )
110
110
  output = _render_to_string(diag)
111
111
  assert "Retry discipline" in output or "CLAUDE.md" in output
112
+
113
+
114
+ def test_verdict_shown_in_output():
115
+ diag = _make_diagnosis([_make_finding("retry_loop")])
116
+ output = _render_to_string(diag)
117
+ assert "Verdict" in output
118
+
119
+
120
+ def test_verdict_clean_shown():
121
+ output = _render_to_string(_make_diagnosis())
122
+ assert "clean" in output.lower()
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # render_turn (#76)
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ def _make_trace_with_turn(turn_number=3, role="assistant", text="some content"):
131
+ from datetime import datetime, timezone
132
+
133
+ from cctx.models import SessionTrace, Turn
134
+
135
+ t = Turn(
136
+ turn_number=turn_number,
137
+ uuid=f"uuid-{turn_number}",
138
+ parent_uuid=None,
139
+ role=role,
140
+ text=text,
141
+ thinking="",
142
+ tool_uses=[],
143
+ tool_results=[],
144
+ usage=None,
145
+ model="claude-sonnet-4-6",
146
+ stop_reason="end_turn",
147
+ timestamp=datetime(2026, 5, 14, 10, 30, 0, tzinfo=timezone.utc),
148
+ duration_ms=None,
149
+ )
150
+ from pathlib import Path
151
+ return SessionTrace(
152
+ session_id="trace-test",
153
+ parent_session_id=None,
154
+ project_path="/p",
155
+ cwd="/p",
156
+ primary_model="claude-sonnet-4-6",
157
+ claude_code_version=None,
158
+ turns=[t],
159
+ subagents=[],
160
+ attachments=[],
161
+ raw_tool_result_files=[],
162
+ initial_context_tokens=0,
163
+ tool_names_loaded=[],
164
+ start_time=t.timestamp,
165
+ end_time=t.timestamp,
166
+ source_path=Path("/p/trace-test.jsonl"),
167
+ subagent_meta={},
168
+ warnings=[],
169
+ subagent_parse_errors=[],
170
+ )
171
+
172
+
173
+ def _render_turn_to_string(trace, diagnosis, turn_num):
174
+ from rich.console import Console
175
+
176
+ from cctx.renderers.terminal import render_turn
177
+
178
+ buf = StringIO()
179
+ console = Console(file=buf, width=120, highlight=False, markup=False)
180
+ render_turn(trace, diagnosis, turn_num, console=console)
181
+ return buf.getvalue()
182
+
183
+
184
+ def test_render_turn_shows_role_and_number():
185
+ trace = _make_trace_with_turn(turn_number=3, role="assistant", text="hello world")
186
+ diag = _make_diagnosis()
187
+ output = _render_turn_to_string(trace, diag, 3)
188
+ assert "Turn 3" in output
189
+ assert "assistant" in output
190
+
191
+
192
+ def test_render_turn_shows_text():
193
+ trace = _make_trace_with_turn(text="important content here")
194
+ diag = _make_diagnosis()
195
+ output = _render_turn_to_string(trace, diag, 3)
196
+ assert "important content here" in output
197
+
198
+
199
+ def test_render_turn_not_found():
200
+ trace = _make_trace_with_turn(turn_number=3)
201
+ diag = _make_diagnosis()
202
+ output = _render_turn_to_string(trace, diag, 99)
203
+ assert "not found" in output.lower() or "99" in output
204
+
205
+
206
+ def test_render_turn_shows_active_finding():
207
+ trace = _make_trace_with_turn(turn_number=5)
208
+ finding = _make_finding("retry_loop") # first_turn=5, last_turn=10
209
+ diag = _make_diagnosis([finding])
210
+ output = _render_turn_to_string(trace, diag, 5)
211
+ assert "retry" in output.lower() or "RETRY" in output
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes