cctx-cli 1.10.0__tar.gz → 1.11.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 (180) hide show
  1. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/CHANGELOG.md +15 -0
  2. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/__init__.py +1 -1
  4. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/cli.py +71 -2
  5. cctx_cli-1.11.0/cctx/hook_installer.py +101 -0
  6. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/pyproject.toml +1 -1
  7. cctx_cli-1.11.0/tests/test_init.py +323 -0
  8. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.github/workflows/ci.yml +0 -0
  9. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.github/workflows/publish.yml +0 -0
  10. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.github/workflows/release.yml +0 -0
  11. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/.gitignore +0 -0
  12. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/CLAUDE.md +0 -0
  13. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/DESIGN.md +0 -0
  14. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/PRODUCT.md +0 -0
  15. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/README.md +0 -0
  16. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/action.yml +0 -0
  17. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/agents.py +0 -0
  18. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/__init__.py +0 -0
  19. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/aggregate.py +0 -0
  20. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/inflection.py +0 -0
  21. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  22. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  23. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/fan_out.py +0 -0
  24. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
  25. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  26. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  27. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  28. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  29. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/discovery.py +0 -0
  30. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/__init__.py +0 -0
  31. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/csv.py +0 -0
  32. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/json.py +0 -0
  33. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/exporters/jsonl.py +0 -0
  34. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/harvest.py +0 -0
  35. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/models.py +0 -0
  36. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/parsers/__init__.py +0 -0
  37. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/parsers/claude_code.py +0 -0
  38. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/pricing.py +0 -0
  39. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/recommender/__init__.py +0 -0
  40. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/recommender/claude_md.py +0 -0
  41. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/recommender/evidence.py +0 -0
  42. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/__init__.py +0 -0
  43. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/github.py +0 -0
  44. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/report.py +0 -0
  45. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  46. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/terminal.py +0 -0
  47. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/renderers/trace_tui.py +0 -0
  48. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/tokenizer.py +0 -0
  49. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx/watcher.py +0 -0
  50. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/cctx-project-brief.md +0 -0
  51. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/demo.gif +0 -0
  52. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/demo.tape +0 -0
  53. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  54. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  55. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  56. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
  57. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  58. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  59. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  60. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
  61. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  62. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
  63. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  64. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  65. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  66. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  67. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  68. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
  69. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  70. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
  71. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
  72. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/__init__.py +0 -0
  73. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/conftest.py +0 -0
  74. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/__init__.py +0 -0
  75. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/conftest.py +0 -0
  76. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_dead_end.py +0 -0
  77. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_inflection.py +0 -0
  78. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_orchestrator.py +0 -0
  79. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_project_specific.py +0 -0
  80. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_retry_loop.py +0 -0
  81. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_scope_creep.py +0 -0
  82. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_stale_context.py +0 -0
  83. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  84. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/exporters/__init__.py +0 -0
  85. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/exporters/test_csv.py +0 -0
  86. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/exporters/test_jsonl.py +0 -0
  87. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/README.md +0 -0
  88. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  89. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  90. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  91. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  92. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  93. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  94. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  95. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  96. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  97. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  98. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  99. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  100. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  101. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  102. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  103. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  104. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  105. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  106. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  107. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  108. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  109. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  110. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  111. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  112. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  113. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  114. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  115. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  116. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  117. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  118. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  119. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  120. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  121. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  122. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  123. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  124. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  125. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  126. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  127. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  128. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  129. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  130. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  131. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  132. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  133. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  134. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  135. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  136. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  137. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  138. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  139. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  140. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  141. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  142. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  143. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  144. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  145. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  146. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/scrub.py +0 -0
  147. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  148. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  149. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  150. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  151. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  152. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/parsers/__init__.py +0 -0
  153. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/parsers/test_claude_code.py +0 -0
  154. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/parsers/test_claude_code_integration.py +0 -0
  155. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/recommender/__init__.py +0 -0
  156. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/recommender/test_claude_md.py +0 -0
  157. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/recommender/test_evidence.py +0 -0
  158. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/renderers/__init__.py +0 -0
  159. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/renderers/test_report.py +0 -0
  160. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  161. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_agents.py +0 -0
  162. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_aggregate.py +0 -0
  163. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_cli.py +0 -0
  164. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_cli_export.py +0 -0
  165. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_diagnostician_subagents.py +0 -0
  166. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_discovery.py +0 -0
  167. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_efficacy.py +0 -0
  168. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_fanout_classifier.py +0 -0
  169. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_github_summary.py +0 -0
  170. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_harvest.py +0 -0
  171. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_harvest_check.py +0 -0
  172. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_harvest_emit.py +0 -0
  173. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_models.py +0 -0
  174. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_models_project_pattern.py +0 -0
  175. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_recommender.py +0 -0
  176. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_smoke.py +0 -0
  177. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_terminal_renderer.py +0 -0
  178. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_tokenizer.py +0 -0
  179. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_trace_tui.py +0 -0
  180. {cctx_cli-1.10.0 → cctx_cli-1.11.0}/tests/test_watcher.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.11.0 (2026-06-11)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Ruff E501 + I001 in test_init.py (line length + import order)
10
+ ([#113](https://github.com/jacquardlabs/cctx/pull/113),
11
+ [`a04426e`](https://github.com/jacquardlabs/cctx/commit/a04426ef62cfeaa4ba7ea392bdf0dd49975cffbb))
12
+
13
+ ### Features
14
+
15
+ - Cctx init — SessionEnd hook installer + autopsy --quiet (closes #92)
16
+ ([#113](https://github.com/jacquardlabs/cctx/pull/113),
17
+ [`a04426e`](https://github.com/jacquardlabs/cctx/commit/a04426ef62cfeaa4ba7ea392bdf0dd49975cffbb))
18
+
19
+
5
20
  ## v1.10.0 (2026-06-11)
6
21
 
7
22
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.10.0
3
+ Version: 1.11.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.10.0"
3
+ __version__ = "1.11.0"
@@ -9,6 +9,7 @@ Commands:
9
9
  cctx harvest <session> Apply autopsy patches to CLAUDE.md
10
10
  cctx harvest <project> --since Cross-session harvest
11
11
  cctx watch [project] Live waste signals during an active session
12
+ cctx init Install opt-in SessionEnd hook for auto-diagnostics
12
13
  """
13
14
  from __future__ import annotations
14
15
 
@@ -292,7 +293,15 @@ def ls(project: Path | None) -> None:
292
293
  "json_out",
293
294
  is_flag=True,
294
295
  default=False,
295
- help="Output diagnosis as JSON to stdout (single-session only).",
296
+ help="Output diagnosis (or aggregate) as JSON to stdout.",
297
+ )
298
+ @click.option(
299
+ "--quiet",
300
+ "quiet",
301
+ is_flag=True,
302
+ default=False,
303
+ help="Print one verdict line only when findings exist; nothing if clean. "
304
+ "Designed for SessionEnd hook use (cctx init).",
296
305
  )
297
306
  def autopsy(
298
307
  target: Path | None,
@@ -305,6 +314,7 @@ def autopsy(
305
314
  top_n: int | None,
306
315
  turn_num: int | None,
307
316
  json_out: bool,
317
+ quiet: bool,
308
318
  ) -> None:
309
319
  """Diagnose a session or project directory.
310
320
 
@@ -408,7 +418,11 @@ def autopsy(
408
418
  trace = tokenize_session(parse_session(target))
409
419
  diagnosis = diagnostician.run(trace)
410
420
  diagnosis = claude_md.generate(diagnosis)
411
- if json_out:
421
+ if quiet:
422
+ if diagnosis.findings:
423
+ kinds = list(dict.fromkeys(f.kind.value for f in diagnosis.findings))
424
+ click.echo(f"{len(diagnosis.findings)} finding(s): {', '.join(kinds)}")
425
+ elif json_out:
412
426
  import json as _json
413
427
 
414
428
  from cctx.exporters.jsonl import export_diagnosis as _export_diag
@@ -729,3 +743,58 @@ def watch(target: Path | None) -> None:
729
743
  """
730
744
  from cctx.watcher import watch as _watch
731
745
  _watch(target)
746
+
747
+
748
+ @cli.command("init")
749
+ @click.option(
750
+ "--global",
751
+ "global_",
752
+ is_flag=True,
753
+ default=False,
754
+ help="Install to ~/.claude/settings.json (user scope) instead of .claude/settings.json.",
755
+ )
756
+ @click.option(
757
+ "--remove",
758
+ "remove_",
759
+ is_flag=True,
760
+ default=False,
761
+ help="Remove the SessionEnd hook instead of installing it.",
762
+ )
763
+ @click.option(
764
+ "--force",
765
+ is_flag=True,
766
+ default=False,
767
+ help="Reinstall even if hook is already present.",
768
+ )
769
+ def init_cmd(global_: bool, remove_: bool, force: bool) -> None:
770
+ """Install an opt-in SessionEnd hook for automatic post-session diagnostics.
771
+
772
+ Writes a hook to .claude/settings.json (project) or ~/.claude/settings.json
773
+ (--global) that runs 'cctx autopsy --latest --quiet' when a Claude Code
774
+ session ends. Output appears only when findings exist.
775
+
776
+ Idempotent — running twice does not duplicate the hook.
777
+ """
778
+ from cctx import hook_installer
779
+
780
+ if force and remove_:
781
+ raise click.UsageError("--force and --remove are mutually exclusive.")
782
+
783
+ scope = "~/.claude/settings.json" if global_ else ".claude/settings.json"
784
+
785
+ if remove_:
786
+ path = hook_installer.remove(global_=global_)
787
+ if path is None:
788
+ click.echo(f"No cctx hook found in {scope} — nothing to remove.")
789
+ else:
790
+ click.echo(f"✓ SessionEnd hook removed from {scope}")
791
+ return
792
+
793
+ result = hook_installer.install(global_=global_, force=force)
794
+ if result == "already_installed":
795
+ click.echo(f"! SessionEnd hook already installed in {scope}")
796
+ click.echo(" Use 'cctx init --force' to reinstall.")
797
+ else:
798
+ click.echo(f"✓ SessionEnd hook installed to {scope}")
799
+ remove_flag = "--global --remove" if global_ else "--remove"
800
+ click.echo(f" Run 'cctx init {remove_flag}' to uninstall.")
@@ -0,0 +1,101 @@
1
+ """Settings-merge hook installer — install/remove the cctx SessionEnd hook.
2
+
3
+ Reads, merges, and writes ~/.claude/settings.json or .claude/settings.json
4
+ without touching any other keys. Idempotent: fingerprinted by the hook's
5
+ description field so a double-install is a no-op.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ HOOK_DESCRIPTION = "cctx SessionEnd hook (diagnostics on session exit)"
14
+ HOOK_COMMAND = "cctx autopsy --latest --quiet"
15
+
16
+
17
+ def settings_path(global_: bool) -> Path:
18
+ if global_:
19
+ return Path.home() / ".claude" / "settings.json"
20
+ return Path(".claude") / "settings.json"
21
+
22
+
23
+ def _load(path: Path) -> dict[str, Any]:
24
+ if not path.exists():
25
+ return {}
26
+ try:
27
+ return json.loads(path.read_text(encoding="utf-8"))
28
+ except json.JSONDecodeError as exc:
29
+ raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
30
+
31
+
32
+ def _save(path: Path, settings: dict[str, Any]) -> None:
33
+ path.parent.mkdir(parents=True, exist_ok=True)
34
+ tmp = path.with_suffix(".json.tmp")
35
+ tmp.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
36
+ tmp.replace(path)
37
+
38
+
39
+ def _find_hook(session_end: list[dict[str, Any]]) -> int | None:
40
+ """Return the index of the cctx hook group in the SessionEnd array, or None."""
41
+ for i, group in enumerate(session_end):
42
+ for h in group.get("hooks", []):
43
+ desc = h.get("description", "")
44
+ if isinstance(desc, str) and "cctx SessionEnd" in desc:
45
+ return i
46
+ return None
47
+
48
+
49
+ def _hook_entry() -> dict[str, Any]:
50
+ return {
51
+ "hooks": [
52
+ {
53
+ "type": "command",
54
+ "command": HOOK_COMMAND,
55
+ "async": True,
56
+ "description": HOOK_DESCRIPTION,
57
+ }
58
+ ]
59
+ }
60
+
61
+
62
+ def is_installed(global_: bool = False) -> bool:
63
+ path = settings_path(global_)
64
+ settings = _load(path)
65
+ session_end = settings.get("hooks", {}).get("SessionEnd", [])
66
+ return _find_hook(session_end) is not None
67
+
68
+
69
+ def install(global_: bool = False, force: bool = False) -> str:
70
+ """Install the hook. Returns "already_installed" or the path written."""
71
+ path = settings_path(global_)
72
+ settings = _load(path)
73
+ hooks = settings.setdefault("hooks", {})
74
+ session_end = hooks.setdefault("SessionEnd", [])
75
+ idx = _find_hook(session_end)
76
+ if idx is not None and not force:
77
+ return "already_installed"
78
+ if idx is not None:
79
+ session_end[idx] = _hook_entry()
80
+ else:
81
+ session_end.append(_hook_entry())
82
+ _save(path, settings)
83
+ return str(path)
84
+
85
+
86
+ def remove(global_: bool = False) -> str | None:
87
+ """Remove the hook. Returns the path written, or None if not found."""
88
+ path = settings_path(global_)
89
+ settings = _load(path)
90
+ hooks = settings.get("hooks", {})
91
+ session_end = hooks.get("SessionEnd", [])
92
+ idx = _find_hook(session_end)
93
+ if idx is None:
94
+ return None
95
+ session_end.pop(idx)
96
+ if not session_end:
97
+ del hooks["SessionEnd"]
98
+ if not hooks:
99
+ del settings["hooks"]
100
+ _save(path, settings)
101
+ return str(path)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cctx-cli"
7
- version = "1.10.0"
7
+ version = "1.11.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"
@@ -0,0 +1,323 @@
1
+ """Tests for cctx init command and hook_installer module."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from click.testing import CliRunner
9
+
10
+
11
+ @pytest.fixture
12
+ def runner():
13
+ return CliRunner()
14
+
15
+
16
+ @pytest.fixture
17
+ def settings_dir(tmp_path):
18
+ d = tmp_path / ".claude"
19
+ d.mkdir()
20
+ return d
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # hook_installer unit tests
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def test_install_creates_settings_file(tmp_path, monkeypatch):
29
+ """install() writes .claude/settings.json with the hook entry."""
30
+ monkeypatch.chdir(tmp_path)
31
+ from cctx import hook_installer
32
+
33
+ result = hook_installer.install()
34
+ path = Path(".claude/settings.json")
35
+ assert path.exists()
36
+ data = json.loads(path.read_text())
37
+ session_end = data["hooks"]["SessionEnd"]
38
+ assert len(session_end) == 1
39
+ inner = session_end[0]["hooks"][0]
40
+ assert inner["type"] == "command"
41
+ assert "cctx autopsy --latest --quiet" in inner["command"]
42
+ assert "cctx SessionEnd" in inner["description"]
43
+ assert result == str(path)
44
+
45
+
46
+ def test_install_idempotent(tmp_path, monkeypatch):
47
+ """Running install() twice does not duplicate the hook."""
48
+ monkeypatch.chdir(tmp_path)
49
+ from cctx import hook_installer
50
+
51
+ hook_installer.install()
52
+ result = hook_installer.install()
53
+ assert result == "already_installed"
54
+
55
+ path = Path(".claude/settings.json")
56
+ data = json.loads(path.read_text())
57
+ assert len(data["hooks"]["SessionEnd"]) == 1
58
+
59
+
60
+ def test_install_preserves_existing_settings(tmp_path, monkeypatch):
61
+ """install() merges into existing settings without clobbering other keys."""
62
+ monkeypatch.chdir(tmp_path)
63
+ from cctx import hook_installer
64
+
65
+ settings_path = tmp_path / ".claude" / "settings.json"
66
+ settings_path.parent.mkdir()
67
+ existing = {
68
+ "permissions": {"allow": ["Bash(git*)"]},
69
+ "hooks": {
70
+ "PreToolUse": [
71
+ {"matcher": "Bash", "hooks": [{"type": "command", "command": "echo pre"}]}
72
+ ]
73
+ },
74
+ }
75
+ settings_path.write_text(json.dumps(existing))
76
+
77
+ hook_installer.install()
78
+ data = json.loads(settings_path.read_text())
79
+
80
+ # Existing keys preserved
81
+ assert data["permissions"]["allow"] == ["Bash(git*)"]
82
+ assert "PreToolUse" in data["hooks"]
83
+ # New hook added
84
+ assert "SessionEnd" in data["hooks"]
85
+ assert len(data["hooks"]["SessionEnd"]) == 1
86
+
87
+
88
+ def test_install_force_replaces_hook(tmp_path, monkeypatch):
89
+ """install(force=True) replaces existing hook without duplicating."""
90
+ monkeypatch.chdir(tmp_path)
91
+ from cctx import hook_installer
92
+
93
+ hook_installer.install()
94
+ result = hook_installer.install(force=True)
95
+
96
+ path = Path(".claude/settings.json")
97
+ data = json.loads(path.read_text())
98
+ assert len(data["hooks"]["SessionEnd"]) == 1
99
+ assert result != "already_installed"
100
+
101
+
102
+ def test_remove_cleans_up(tmp_path, monkeypatch):
103
+ """remove() deletes the hook and prunes empty keys."""
104
+ monkeypatch.chdir(tmp_path)
105
+ from cctx import hook_installer
106
+
107
+ hook_installer.install()
108
+ result = hook_installer.remove()
109
+
110
+ path = Path(".claude/settings.json")
111
+ assert result is not None
112
+ data = json.loads(path.read_text())
113
+ assert "hooks" not in data
114
+
115
+
116
+ def test_remove_preserves_other_hooks(tmp_path, monkeypatch):
117
+ """remove() only removes the cctx hook, leaving other SessionEnd hooks."""
118
+ monkeypatch.chdir(tmp_path)
119
+ from cctx import hook_installer
120
+
121
+ settings_path = tmp_path / ".claude" / "settings.json"
122
+ settings_path.parent.mkdir()
123
+ other_hook = {"hooks": [{"type": "command", "command": "echo other"}]}
124
+ settings_path.write_text(json.dumps({
125
+ "hooks": {"SessionEnd": [other_hook]}
126
+ }))
127
+
128
+ hook_installer.install()
129
+ hook_installer.remove()
130
+
131
+ data = json.loads(settings_path.read_text())
132
+ session_end = data["hooks"]["SessionEnd"]
133
+ assert len(session_end) == 1
134
+ assert session_end[0]["hooks"][0]["command"] == "echo other"
135
+
136
+
137
+ def test_remove_not_found_returns_none(tmp_path, monkeypatch):
138
+ """remove() when hook is not installed returns None without error."""
139
+ monkeypatch.chdir(tmp_path)
140
+ from cctx import hook_installer
141
+
142
+ assert hook_installer.remove() is None
143
+
144
+
145
+ def test_is_installed_false_before_install(tmp_path, monkeypatch):
146
+ monkeypatch.chdir(tmp_path)
147
+ from cctx import hook_installer
148
+
149
+ assert not hook_installer.is_installed()
150
+
151
+
152
+ def test_is_installed_true_after_install(tmp_path, monkeypatch):
153
+ monkeypatch.chdir(tmp_path)
154
+ from cctx import hook_installer
155
+
156
+ hook_installer.install()
157
+ assert hook_installer.is_installed()
158
+
159
+
160
+ def test_invalid_json_raises(tmp_path, monkeypatch):
161
+ """Corrupt settings.json → ValueError, not a silent overwrite."""
162
+ monkeypatch.chdir(tmp_path)
163
+ from cctx import hook_installer
164
+
165
+ path = tmp_path / ".claude" / "settings.json"
166
+ path.parent.mkdir()
167
+ path.write_text("{ not valid json }")
168
+
169
+ with pytest.raises(ValueError, match="Invalid JSON"):
170
+ hook_installer.install()
171
+
172
+
173
+ def test_global_scope_writes_to_home(tmp_path, monkeypatch):
174
+ """--global writes to ~/.claude/settings.json."""
175
+ fake_home = tmp_path / "home"
176
+ fake_home.mkdir()
177
+ monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
178
+ from cctx import hook_installer
179
+
180
+ hook_installer.install(global_=True)
181
+ path = fake_home / ".claude" / "settings.json"
182
+ assert path.exists()
183
+ data = json.loads(path.read_text())
184
+ assert "SessionEnd" in data["hooks"]
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # CLI integration tests (cctx init)
189
+ # ---------------------------------------------------------------------------
190
+
191
+
192
+ def test_init_installs_hook(runner, tmp_path):
193
+ """cctx init writes the hook and prints confirmation."""
194
+ from cctx.cli import cli
195
+
196
+ result = runner.invoke(cli, ["init"], catch_exceptions=False, env={"PWD": str(tmp_path)})
197
+ # Note: monkeypatching cwd is simpler in unit tests; CLI uses Path(".claude/...")
198
+ # relative to cwd — just check exit code and output shape
199
+ assert result.exit_code == 0
200
+ assert (
201
+ "✓" in result.output
202
+ or "already" in result.output.lower()
203
+ or "installed" in result.output.lower()
204
+ )
205
+
206
+
207
+ def test_init_idempotent_cli(runner, tmp_path, monkeypatch):
208
+ """cctx init twice → second run reports already installed."""
209
+ monkeypatch.chdir(tmp_path)
210
+ from cctx.cli import cli
211
+
212
+ runner.invoke(cli, ["init"], catch_exceptions=False)
213
+ result = runner.invoke(cli, ["init"], catch_exceptions=False)
214
+ assert result.exit_code == 0
215
+ assert "already" in result.output.lower()
216
+
217
+
218
+ def test_init_remove_cli(runner, tmp_path, monkeypatch):
219
+ """cctx init --remove cleans up the hook."""
220
+ monkeypatch.chdir(tmp_path)
221
+ from cctx.cli import cli
222
+
223
+ runner.invoke(cli, ["init"], catch_exceptions=False)
224
+ result = runner.invoke(cli, ["init", "--remove"], catch_exceptions=False)
225
+ assert result.exit_code == 0
226
+ assert "removed" in result.output.lower()
227
+
228
+
229
+ def test_init_remove_not_installed_cli(runner, tmp_path, monkeypatch):
230
+ """cctx init --remove when not installed exits 0 with a friendly message."""
231
+ monkeypatch.chdir(tmp_path)
232
+ from cctx.cli import cli
233
+
234
+ result = runner.invoke(cli, ["init", "--remove"], catch_exceptions=False)
235
+ assert result.exit_code == 0
236
+ assert "nothing" in result.output.lower() or "not found" in result.output.lower()
237
+
238
+
239
+ def test_init_force_and_remove_errors(runner, tmp_path, monkeypatch):
240
+ """--force --remove together → UsageError."""
241
+ monkeypatch.chdir(tmp_path)
242
+ from cctx.cli import cli
243
+
244
+ result = runner.invoke(cli, ["init", "--force", "--remove"])
245
+ assert result.exit_code != 0
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # autopsy --quiet tests
250
+ # ---------------------------------------------------------------------------
251
+
252
+
253
+ def _make_session_file(tmp_path: Path, session_id: str) -> Path:
254
+ line = {
255
+ "type": "user",
256
+ "uuid": f"{session_id}-u1",
257
+ "parentUuid": None,
258
+ "isSidechain": False,
259
+ "timestamp": "2026-05-14T10:00:00.000Z",
260
+ "sessionId": session_id,
261
+ "version": "2.1.138",
262
+ "cwd": "/Users/test/Projects/demo",
263
+ "gitBranch": "main",
264
+ "userType": "external",
265
+ "entrypoint": "cli",
266
+ "message": {"role": "user", "content": "hello"},
267
+ }
268
+ path = tmp_path / f"{session_id}.jsonl"
269
+ path.write_text(json.dumps(line) + "\n")
270
+ return path
271
+
272
+
273
+ def test_autopsy_quiet_clean_no_output(runner, tmp_path):
274
+ """--quiet on a clean session emits nothing and exits 0."""
275
+ from cctx.cli import cli
276
+
277
+ session = _make_session_file(tmp_path, "quiet-clean-01")
278
+ result = runner.invoke(cli, ["autopsy", str(session), "--quiet"], catch_exceptions=False)
279
+ assert result.exit_code == 0
280
+ assert result.output.strip() == ""
281
+
282
+
283
+ def test_autopsy_quiet_with_findings_emits_verdict(runner, tmp_path, monkeypatch):
284
+ """--quiet on a session with findings emits one verdict line."""
285
+ from cctx.cli import cli
286
+ from cctx.models import Confidence, Finding, FindingKind, Severity
287
+
288
+ session = _make_session_file(tmp_path, "quiet-dirty-01")
289
+
290
+ # Inject a finding via monkeypatch so we don't need a real problematic session
291
+ from datetime import datetime, timezone
292
+
293
+ from cctx import diagnostician
294
+ from cctx.models import Diagnosis
295
+
296
+ def fake_run(trace):
297
+ return Diagnosis(
298
+ session_id=trace.session_id,
299
+ findings=[
300
+ Finding(
301
+ kind=FindingKind.RETRY_LOOP,
302
+ severity=Severity.HIGH,
303
+ confidence=Confidence.HIGH,
304
+ first_turn=1,
305
+ last_turn=2,
306
+ evidence={},
307
+ cost_usd=0.01,
308
+ summary="test finding",
309
+ )
310
+ ],
311
+ inflection_turn=1,
312
+ patches=[],
313
+ total_cost_usd=0.10,
314
+ waste_cost_usd=0.01,
315
+ analysed_at=datetime(2026, 5, 14, 10, 0, tzinfo=timezone.utc),
316
+ )
317
+
318
+ monkeypatch.setattr(diagnostician, "run", fake_run)
319
+
320
+ result = runner.invoke(cli, ["autopsy", str(session), "--quiet"], catch_exceptions=False)
321
+ assert result.exit_code == 0
322
+ assert "finding" in result.output.lower()
323
+ assert "retry_loop" in result.output.lower()
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