cctx-cli 1.4.0__tar.gz → 1.5.1__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 (174) hide show
  1. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/CHANGELOG.md +51 -0
  2. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/PKG-INFO +1 -1
  3. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/PRODUCT.md +23 -12
  4. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/__init__.py +1 -1
  5. cctx_cli-1.5.1/cctx/agents.py +66 -0
  6. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/cli.py +5 -2
  7. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/recommender/claude_md.py +21 -3
  8. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/renderers/terminal.py +35 -4
  9. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/watcher.py +37 -11
  10. cctx_cli-1.5.1/docs/product-reviews/2026-06-09-product-review.md +186 -0
  11. cctx_cli-1.5.1/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +839 -0
  12. cctx_cli-1.5.1/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +128 -0
  13. cctx_cli-1.5.1/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +198 -0
  14. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/pyproject.toml +1 -1
  15. cctx_cli-1.5.1/tests/test_agents.py +128 -0
  16. cctx_cli-1.5.1/tests/test_recommender.py +125 -0
  17. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_terminal_renderer.py +154 -0
  18. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_watcher.py +101 -0
  19. cctx_cli-1.4.0/tests/test_recommender.py +0 -56
  20. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/.github/workflows/ci.yml +0 -0
  21. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/.github/workflows/publish.yml +0 -0
  22. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/.github/workflows/release.yml +0 -0
  23. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/.gitignore +0 -0
  24. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/CLAUDE.md +0 -0
  25. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/DESIGN.md +0 -0
  26. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/README.md +0 -0
  27. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/action.yml +0 -0
  28. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/__init__.py +0 -0
  29. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/aggregate.py +0 -0
  30. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/inflection.py +0 -0
  31. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/patterns/__init__.py +0 -0
  32. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/patterns/dead_end.py +0 -0
  33. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/patterns/project_specific.py +0 -0
  34. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  35. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  36. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/patterns/stale_context.py +0 -0
  37. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  38. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/discovery.py +0 -0
  39. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/exporters/__init__.py +0 -0
  40. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/exporters/csv.py +0 -0
  41. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/exporters/json.py +0 -0
  42. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/exporters/jsonl.py +0 -0
  43. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/harvest.py +0 -0
  44. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/models.py +0 -0
  45. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/parsers/__init__.py +0 -0
  46. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/parsers/claude_code.py +0 -0
  47. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/pricing.py +0 -0
  48. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/recommender/__init__.py +0 -0
  49. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/recommender/evidence.py +0 -0
  50. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/renderers/__init__.py +0 -0
  51. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/renderers/github.py +0 -0
  52. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/renderers/report.py +0 -0
  53. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  54. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/renderers/trace_tui.py +0 -0
  55. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx/tokenizer.py +0 -0
  56. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/cctx-project-brief.md +0 -0
  57. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/demo.gif +0 -0
  58. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/demo.tape +0 -0
  59. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  60. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  61. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  62. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  63. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  64. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  65. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
  66. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  67. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  68. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  69. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  70. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  71. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  72. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
  73. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  74. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/__init__.py +0 -0
  75. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/conftest.py +0 -0
  76. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/__init__.py +0 -0
  77. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/conftest.py +0 -0
  78. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_dead_end.py +0 -0
  79. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_inflection.py +0 -0
  80. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_orchestrator.py +0 -0
  81. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_project_specific.py +0 -0
  82. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_retry_loop.py +0 -0
  83. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_scope_creep.py +0 -0
  84. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_stale_context.py +0 -0
  85. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/diagnostician/test_tool_thrash.py +0 -0
  86. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/exporters/__init__.py +0 -0
  87. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/exporters/test_csv.py +0 -0
  88. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/exporters/test_jsonl.py +0 -0
  89. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/README.md +0 -0
  90. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  91. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  92. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  93. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  94. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  95. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  96. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  97. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  98. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  99. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  100. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  101. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  102. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  103. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  104. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  105. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  106. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  107. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  108. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  109. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  110. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  111. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  112. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  113. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  114. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  115. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  116. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  117. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  118. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  119. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  120. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  121. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  122. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  123. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  124. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  125. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  126. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  127. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  128. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  129. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  130. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  131. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  132. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  133. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  134. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  135. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  136. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  137. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  138. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  139. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  140. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  141. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  142. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  143. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  144. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  145. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  146. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  147. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  148. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/scrub.py +0 -0
  149. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  150. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  151. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  152. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  153. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  154. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/parsers/__init__.py +0 -0
  155. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/parsers/test_claude_code.py +0 -0
  156. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/parsers/test_claude_code_integration.py +0 -0
  157. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/recommender/__init__.py +0 -0
  158. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/recommender/test_claude_md.py +0 -0
  159. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/recommender/test_evidence.py +0 -0
  160. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/renderers/__init__.py +0 -0
  161. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/renderers/test_report.py +0 -0
  162. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/renderers/test_terminal_renderer_full.py +0 -0
  163. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_aggregate.py +0 -0
  164. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_cli.py +0 -0
  165. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_cli_export.py +0 -0
  166. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_discovery.py +0 -0
  167. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_github_summary.py +0 -0
  168. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_harvest.py +0 -0
  169. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_harvest_check.py +0 -0
  170. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_models.py +0 -0
  171. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_models_project_pattern.py +0 -0
  172. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_smoke.py +0 -0
  173. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_tokenizer.py +0 -0
  174. {cctx_cli-1.4.0 → cctx_cli-1.5.1}/tests/test_trace_tui.py +0 -0
@@ -2,6 +2,57 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.5.1 (2026-06-10)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Recommender — add TOOL_THRASH/DEAD_END patch templates
10
+ ([#107](https://github.com/jacquardlabs/cctx/pull/107),
11
+ [`3c79d58`](https://github.com/jacquardlabs/cctx/commit/3c79d58a55af106d3fd81542e70b8f892569185c))
12
+
13
+ ### Documentation
14
+
15
+ - Product review 2026-06-09 + M15 cross-agent emit spec
16
+ ([#107](https://github.com/jacquardlabs/cctx/pull/107),
17
+ [`3c79d58`](https://github.com/jacquardlabs/cctx/commit/3c79d58a55af106d3fd81542e70b8f892569185c))
18
+
19
+
20
+ ## v1.5.0 (2026-05-20)
21
+
22
+ ### Bug Fixes
23
+
24
+ - Agents.py — guard against non-list JSON, tighten patch targets
25
+ ([`c41c42c`](https://github.com/jacquardlabs/cctx/commit/c41c42cb366904fc332638b8f97ee042f17b45c2))
26
+
27
+ - Renderer — guard order, tmp_path fixtures, missing no-badge tests
28
+ ([`8f26afd`](https://github.com/jacquardlabs/cctx/commit/8f26afdf27c63641942b434e47a2fb7b24d38068))
29
+
30
+ - Watcher — hermetic tests, reuse _encode_path, rename clarity
31
+ ([`a7b52de`](https://github.com/jacquardlabs/cctx/commit/a7b52def47b13447d956476a0ec6782fe2d0247b))
32
+
33
+ ### Documentation
34
+
35
+ - Implementation plan for claude agents live integration
36
+ ([`2136adf`](https://github.com/jacquardlabs/cctx/commit/2136adf0697a695d27f438f0368e7bd5ba406e89))
37
+
38
+ - Spec for claude agents --json live session integration
39
+ ([`0df2381`](https://github.com/jacquardlabs/cctx/commit/0df23813a90c66e57d0d39c6b959859b89a5c057))
40
+
41
+ ### Features
42
+
43
+ - Add agents.py — live_sessions() via claude agents --json
44
+ ([`83b704f`](https://github.com/jacquardlabs/cctx/commit/83b704ffbe4303dbd316257a01eb0b59307c0e06))
45
+
46
+ - Cctx ls — pass live_statuses to renderer for live session badges
47
+ ([`65445d7`](https://github.com/jacquardlabs/cctx/commit/65445d75a213bf26401fe044a2409d2ce0efbcb1))
48
+
49
+ - Render_sessions/render_projects — live status badges via live_statuses param
50
+ ([`3d77687`](https://github.com/jacquardlabs/cctx/commit/3d776879c9ce03a622361c2b804e5986d40f37a1))
51
+
52
+ - Watcher — live session detection + early idle exit via claude agents --json
53
+ ([`a11481c`](https://github.com/jacquardlabs/cctx/commit/a11481c2126fead87e1f92bfecc7bf5ac3f39d1a))
54
+
55
+
5
56
  ## v1.4.0 (2026-05-20)
6
57
 
7
58
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.4.0
3
+ Version: 1.5.1
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
@@ -4,7 +4,7 @@
4
4
 
5
5
  cctx is a local Python CLI that reads Claude Code session logs and tells you what went wrong, why it cost what it did, and what to add to your CLAUDE.md so it doesn't happen again.
6
6
 
7
- It is a forensic tool, not a monitoring tool. You reach for it after a session — when something felt off, when the bill was higher than expected, or on a weekly review pass.
7
+ It is forensic-first. You reach for it after a session — when something felt off, when the bill was higher than expected, or on a weekly review pass. `cctx watch` extends the same waste detection into a live, opt-in companion for an active session; it is a foreground command you run deliberately, not background infrastructure.
8
8
 
9
9
  ## Primary persona
10
10
 
@@ -20,7 +20,7 @@ This persona is already the target; everything shipped to date serves them direc
20
20
  ## What cctx is NOT for
21
21
 
22
22
  - Teams or organizations (no shared reports, no access control, no multi-user state)
23
- - Real-time monitoring (completed sessions only in v0 and v1)
23
+ - Ambient monitoring (no daemons, no persistent watcher state, no alerting — `cctx watch` is a foreground command you start and stop)
24
24
  - General cost dashboards (CodeBurn covers that; cctx is forensic)
25
25
  - Multi-provider support (Claude Code only in v0/v1)
26
26
  - Users who do not read session transcripts or maintain CLAUDE.md
@@ -28,7 +28,7 @@ This persona is already the target; everything shipped to date serves them direc
28
28
  ## Product principles
29
29
 
30
30
  **1. Forensic, not ambient.**
31
- cctx is used after something goes wrong, not as background infrastructure. Every feature should be justified by this use case: "I just ran a session, something went sideways, I want to know what."
31
+ cctx is forensic-first: the core use case is "I just ran a session, something went sideways, I want to know what." Live mode (`watch`) is justified only as the same detection running earlier it surfaces the identical findings, never becomes a daemon, and never holds state between runs.
32
32
 
33
33
  **2. Output must be actionable.**
34
34
  Every report should produce at least one thing the user can do immediately. Findings without patches are incomplete. Patches must be copy-pasteable or auto-applicable.
@@ -37,15 +37,15 @@ Every report should produce at least one thing the user can do immediately. Find
37
37
  Costs are approximated (85–95% of actual). Pattern classifiers fire only on high-confidence signals. Low-confidence signals are shown with explicit confidence labels. "System internals" token budget is never hidden or misattributed.
38
38
 
39
39
  **4. Local and stateless.**
40
- No network calls in the core analysis path (tokenizer may call the API for token counting; that is opt-in). No persistent database. No telemetry. No account. Users own their data.
40
+ No network calls in the core analysis path (tokenizer may call the API for token counting; that is opt-in). Live-session detection may shell out to the local `claude` CLI and degrades gracefully without it. No persistent database. No telemetry. No account. Users own their data.
41
41
 
42
42
  **5. Deterministic.**
43
43
  Pattern classifiers are heuristics, not LLM calls. The same session file produces the same output every time. Testable on fixtures. Predictable cost to run.
44
44
 
45
45
  **6. Small surface, deep on each command.**
46
- Four commands. No command is shallow. Users should be able to learn the product in an afternoon and trust what it tells them.
46
+ Six commands (`ls`, `autopsy`, `harvest`, `watch`, `trace`, `export`). No command is shallow. Users should be able to learn the product in an afternoon and trust what it tells them.
47
47
 
48
- ## Feature map (v0.2.0)
48
+ ## Feature map (v1.4.0)
49
49
 
50
50
  ### Shipped
51
51
 
@@ -68,8 +68,16 @@ Four commands. No command is shallow. Users should be able to learn the product
68
68
  | Cross-session harvest | `cctx harvest <project> --since N` | M5 |
69
69
  | Session discovery | `cctx ls` / `cctx autopsy --latest` | M6+ |
70
70
  | Live waste signals | `cctx watch <project>` | M6+ |
71
+ | Verdict headline + `--top N` + `--turn N` | `autopsy` | v1.1.0 (M9) |
72
+ | `--until DATE` on cross-session mode | `autopsy --since ... --until` | v1.2.0 (M12) |
73
+ | Machine-readable diagnosis | `cctx autopsy <session> --json` | v1.2.0 (M12) |
74
+ | JSON export | `cctx export <session> --format json` | v1.2.0 (M12) |
75
+ | Project-specific pattern detection | `autopsy`/`harvest` `--since` | v1.3.0 (M14) |
76
+ | Memory-hygiene depth | `harvest --check` + `--check-severity` | v1.4.0 (M13) |
77
+ | Live session badges | `cctx ls` | unreleased |
78
+ | Live session detection, early idle exit | `cctx watch` | unreleased |
71
79
 
72
- ### Pattern classifiers (v0.2.0)
80
+ ### Pattern classifiers (v1.4.0)
73
81
 
74
82
  | Pattern | Status |
75
83
  |---|---|
@@ -78,6 +86,7 @@ Four commands. No command is shallow. Users should be able to learn the product
78
86
  | Stale context | Shipped |
79
87
  | Dead-end exploration | Shipped (v0.2.0) |
80
88
  | Tool thrashing | Shipped (v0.2.0) |
89
+ | Project-specific patterns (cross-session) | Shipped (v1.3.0) |
81
90
 
82
91
  ## What we are NOT building
83
92
 
@@ -87,12 +96,14 @@ Four commands. No command is shallow. Users should be able to learn the product
87
96
  - A fork-and-replay debugger
88
97
  - A general eval or testing framework
89
98
 
90
- ## Known problems (as of 2026-05-16)
99
+ ## Known problems (as of 2026-06-09)
91
100
 
92
- **Active gaps (non-blocking for v0.2.0 but worth tracking):**
101
+ **Active gaps:**
93
102
 
94
- 1. **`cctx watch` polling is simple.** Polls every 1s and re-runs classifiers on any file growth. Does not debounce or use `fsevents`/`inotify`. Fine for v0 but will chatter on active sessions.
103
+ 1. **`cctx watch` polling is simple.** Early idle exit via `claude agents --json` has landed, but the watcher still polls at 1s without `fsevents`/`inotify` debouncing.
95
104
 
96
- 2. **`--format json` on `export` not shipped.** `--html` moved to `autopsy --html`; `json` format on the `export` subcommand is still deferred.
105
+ 2. **Subagent traces are parsed but never diagnosed.** The parser models subagent sessions recursively and the tokenizer counts their tokens, but no classifier or cost attribution reads `trace.subagents`. Autopsy is blind to spend inside agent fan-outs. (M16)
97
106
 
98
- 3. **Cross-agent layer not started.** Emitting findings as `.cursorrules`, `AGENTS.md`, `.windsurfrules`, or GitHub Copilot instructions is a roadmap item with no milestone yet.
107
+ 3. **Cross-agent layer not started.** Tracked as M15 / #82 the final step of the original growth staircase.
108
+
109
+ 4. **Harvest has no feedback loop.** Nothing measures whether an applied patch reduced the recurrence of the pattern it targeted, even though patches carry fingerprints and sessions carry dates. (M17)
@@ -1,3 +1,3 @@
1
1
  """cctx: profile, debug, and optimize Claude Code and Agent SDK sessions."""
2
2
 
3
- __version__ = "1.4.0"
3
+ __version__ = "1.5.1"
@@ -0,0 +1,66 @@
1
+ """Live Claude Code agent query via `claude agents --json`.
2
+
3
+ Public API:
4
+ live_sessions() -> list[LiveSession]
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import subprocess
10
+ from dataclasses import dataclass
11
+ from datetime import datetime, timezone
12
+
13
+
14
+ @dataclass
15
+ class LiveSession:
16
+ session_id: str # matches JSONL filename stem in ~/.claude/projects/
17
+ cwd: str
18
+ status: str # "busy" | "idle"
19
+ pid: int
20
+ kind: str # "interactive" | "background"
21
+ started_at: datetime
22
+
23
+
24
+ def live_sessions() -> list[LiveSession]:
25
+ """Query `claude agents --json`. Returns [] on any failure."""
26
+ try:
27
+ result = subprocess.run(
28
+ ["claude", "agents", "--json"],
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=2,
32
+ )
33
+ except FileNotFoundError:
34
+ return []
35
+ except subprocess.TimeoutExpired:
36
+ return []
37
+
38
+ if result.returncode != 0:
39
+ return []
40
+
41
+ try:
42
+ data = json.loads(result.stdout)
43
+ except json.JSONDecodeError:
44
+ return []
45
+
46
+ if not isinstance(data, list):
47
+ return []
48
+
49
+ sessions: list[LiveSession] = []
50
+ for item in data:
51
+ try:
52
+ sessions.append(
53
+ LiveSession(
54
+ session_id=item["sessionId"],
55
+ cwd=item["cwd"],
56
+ status=item.get("status", "unknown"),
57
+ pid=int(item["pid"]),
58
+ kind=item.get("kind", "interactive"),
59
+ started_at=datetime.fromtimestamp(
60
+ item["startedAt"] / 1000, tz=timezone.utc
61
+ ),
62
+ )
63
+ )
64
+ except (KeyError, TypeError, ValueError):
65
+ continue
66
+ return sessions
@@ -19,6 +19,7 @@ from typing import IO
19
19
  import rich_click as click
20
20
 
21
21
  from cctx import diagnostician
22
+ from cctx.agents import live_sessions as _live_sessions
22
23
  from cctx.diagnostician import aggregate
23
24
  from cctx.diagnostician.patterns import project_specific
24
25
  from cctx.discovery import complete_project as _complete_project
@@ -196,9 +197,11 @@ def ls(project: Path | None) -> None:
196
197
  """
197
198
  from cctx.discovery import ProjectInfo, find_project_dir, list_projects, list_sessions
198
199
 
200
+ live_statuses = {s.session_id: s.status for s in _live_sessions()}
201
+
199
202
  if project is None:
200
203
  projects = list_projects()
201
- render_projects(projects)
204
+ render_projects(projects, live_statuses=live_statuses)
202
205
  else:
203
206
  cwd = project if project.is_dir() else project.parent
204
207
  project_dir = find_project_dir(cwd)
@@ -213,7 +216,7 @@ def ls(project: Path | None) -> None:
213
216
  display_name=str(cwd).replace(str(Path.home()), "~"),
214
217
  sessions=sessions,
215
218
  )
216
- render_sessions(info)
219
+ render_sessions(info, live_statuses=live_statuses)
217
220
 
218
221
 
219
222
  @cli.command()
@@ -41,11 +41,29 @@ _STALE_CONTEXT_DIFF = """\
41
41
  +without re-referencing it. Prefer re-running the tool over dragging stale context
42
42
  +forward — the compaction system handles removal."""
43
43
 
44
+ _TOOL_THRASH_DIFF = """\
45
+ +## Tool-call discipline
46
+ +
47
+ +Before reaching for a tool, decide what specific information the call must return
48
+ +and how it changes the next step. Don't fan out near-identical searches or re-read
49
+ +the same file from different angles hoping something turns up. If two or three calls
50
+ +haven't moved you forward, stop and form a hypothesis before the next one."""
51
+
52
+ _DEAD_END_DIFF = """\
53
+ +## Exploration discipline
54
+ +
55
+ +When an approach stops making progress, name the dead end explicitly and back out
56
+ +rather than pushing deeper on a path that isn't working. Re-read the goal, list the
57
+ +approaches already ruled out, and pick a meaningfully different one. Sunk effort on a
58
+ +failing approach is not a reason to continue it."""
59
+
44
60
  _TEMPLATES: dict[FindingKind, tuple[str, str, str]] = {
45
61
  # kind → (description, diff_body, target_file)
46
- FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
47
- FindingKind.SCOPE_CREEP: ("Add scope discipline rule", _SCOPE_CREEP_DIFF, "CLAUDE.md"),
48
- FindingKind.STALE_CONTEXT: ("Add context hygiene rule", _STALE_CONTEXT_DIFF, "CLAUDE.md"),
62
+ FindingKind.RETRY_LOOP: ("Add retry discipline rule", _RETRY_LOOP_DIFF, "CLAUDE.md"),
63
+ FindingKind.SCOPE_CREEP: ("Add scope discipline rule", _SCOPE_CREEP_DIFF, "CLAUDE.md"),
64
+ FindingKind.STALE_CONTEXT: ("Add context hygiene rule", _STALE_CONTEXT_DIFF, "CLAUDE.md"),
65
+ FindingKind.TOOL_THRASH: ("Add tool-call discipline rule", _TOOL_THRASH_DIFF, "CLAUDE.md"),
66
+ FindingKind.DEAD_END: ("Add exploration discipline rule", _DEAD_END_DIFF, "CLAUDE.md"),
49
67
  }
50
68
 
51
69
 
@@ -3,8 +3,8 @@
3
3
  render_diagnosis(diagnosis, console=None) -> None
4
4
  render_aggregate(report, console=None) -> None
5
5
  render_harvest_results(results, dry_run=False, console=None) -> None
6
- render_projects(projects, console=None) -> None
7
- render_sessions(project, console=None) -> None
6
+ render_projects(projects, live_statuses=None, console=None) -> None
7
+ render_sessions(project, live_statuses=None, console=None) -> None
8
8
 
9
9
  Uses rich for formatting. Accepts an optional Console for testing.
10
10
  """
@@ -294,25 +294,44 @@ def render_harvest_results(
294
294
  con.print(f"Applied {applied_count} patch(es).")
295
295
 
296
296
 
297
- def render_projects(projects: list[ProjectInfo], *, console: Console | None = None) -> None:
297
+ def render_projects(
298
+ projects: list[ProjectInfo],
299
+ *,
300
+ live_statuses: dict[str, str] | None = None,
301
+ console: Console | None = None,
302
+ ) -> None:
298
303
  con = console or _default_console()
299
304
 
300
305
  if not projects:
301
306
  con.print("No projects found in ~/.claude/projects/.")
302
307
  return
303
308
 
309
+ _live = live_statuses or {}
310
+ live_project_ids: set[str] = {
311
+ proj.project_dir.name
312
+ for proj in projects
313
+ for s in proj.sessions
314
+ if s.session_id in _live
315
+ }
316
+
304
317
  con.print(Rule("cctx — projects"))
305
318
  table = Table(show_header=True, box=None, pad_edge=False, show_edge=False)
306
319
  table.add_column("Project", style="bold")
307
320
  table.add_column("Sessions", justify="right", style="dim")
308
321
  table.add_column("Last session", style="dim")
322
+ table.add_column("Status")
309
323
 
310
324
  for proj in projects:
311
325
  last = proj.latest_time.strftime("%Y-%m-%d") if proj.latest_time else "—"
326
+ if proj.project_dir.name in live_project_ids:
327
+ status_cell = Text("● live", style="green bold")
328
+ else:
329
+ status_cell = Text("")
312
330
  table.add_row(
313
331
  proj.display_name,
314
332
  str(proj.session_count),
315
333
  last,
334
+ status_cell,
316
335
  )
317
336
  con.print(table)
318
337
  con.print()
@@ -326,8 +345,14 @@ def render_projects(projects: list[ProjectInfo], *, console: Console | None = No
326
345
  )
327
346
 
328
347
 
329
- def render_sessions(project: ProjectInfo, *, console: Console | None = None) -> None:
348
+ def render_sessions(
349
+ project: ProjectInfo,
350
+ *,
351
+ live_statuses: dict[str, str] | None = None,
352
+ console: Console | None = None,
353
+ ) -> None:
330
354
  con = console or _default_console()
355
+ _live = live_statuses or {}
331
356
 
332
357
  con.print(Rule(f"cctx — {project.display_name}"))
333
358
  if not project.sessions:
@@ -339,14 +364,20 @@ def render_sessions(project: ProjectInfo, *, console: Console | None = None) ->
339
364
  table.add_column("Date", style="dim")
340
365
  table.add_column("Branch", style="dim")
341
366
  table.add_column("Path", style="dim")
367
+ table.add_column("Status")
342
368
 
343
369
  for s in project.sessions:
344
370
  date_str = s.start_time.strftime("%Y-%m-%d %H:%M") if s.start_time else "—"
371
+ if s.session_id in _live:
372
+ status_cell = Text(f"● {_live[s.session_id]}", style="green bold")
373
+ else:
374
+ status_cell = Text("")
345
375
  table.add_row(
346
376
  s.session_id[:8],
347
377
  date_str,
348
378
  s.git_branch or "—",
349
379
  str(s.path),
380
+ status_cell,
350
381
  )
351
382
  con.print(table)
352
383
  con.print()
@@ -4,7 +4,7 @@ Public API:
4
4
  watch(target) -> None
5
5
 
6
6
  Layering rules (MUST respect):
7
- - Imports parser and diagnostician only.
7
+ - Imports parser, diagnostician, and agents only.
8
8
  - Does NOT import renderers, click, rich, or anthropic.
9
9
  - Uses a compact single-line formatter defined here (not imported from renderers).
10
10
  """
@@ -16,6 +16,7 @@ import time
16
16
  from pathlib import Path
17
17
 
18
18
  from cctx import diagnostician
19
+ from cctx.agents import live_sessions
19
20
  from cctx.models import KIND_LABEL, Finding, FindingKind
20
21
 
21
22
  _POLL_INTERVAL = 1.0 # seconds between size checks
@@ -34,11 +35,22 @@ def _format_finding(f: Finding) -> str:
34
35
 
35
36
 
36
37
  def _find_active_session(project_dir: Path) -> Path | None:
37
- """Return the most recently modified JSONL in project_dir, or None.
38
+ """Return the JSONL for the active session in project_dir.
38
39
 
39
- Uses st_mtime (not embedded session start_time) so that a currently-active
40
- session sorts above older completed sessions regardless of their timestamps.
40
+ Prefers the live session from `claude agents --json` when available;
41
+ falls back to most-recently-modified JSONL.
41
42
  """
43
+ from cctx.discovery import _encode_path
44
+
45
+ encoded_name = project_dir.name
46
+ for live in live_sessions():
47
+ if live.cwd:
48
+ live_encoded = _encode_path(Path(live.cwd))
49
+ if live_encoded == encoded_name:
50
+ candidate = project_dir / f"{live.session_id}.jsonl"
51
+ if candidate.exists():
52
+ return candidate
53
+
42
54
  candidates = list(project_dir.glob("*.jsonl"))
43
55
  if not candidates:
44
56
  return None
@@ -69,6 +81,8 @@ def _tail(session_path: Path) -> int:
69
81
  seen_keys: set[tuple[FindingKind, int]] = set()
70
82
  last_size = 0
71
83
  idle_since: float | None = None
84
+ session_id = session_path.stem
85
+ claude_is_responsive = False # True once we've confirmed claude is available
72
86
 
73
87
  print(f"Watching {session_path.name} … Ctrl+C to stop.", flush=True)
74
88
 
@@ -99,13 +113,25 @@ def _tail(session_path: Path) -> int:
99
113
  now = time.monotonic()
100
114
  if idle_since is None:
101
115
  idle_since = now
102
- elif now - idle_since >= _IDLE_TIMEOUT:
103
- print(
104
- f"\nSession idle for {_IDLE_TIMEOUT:.0f}s analysis complete. "
105
- f"Findings detected: {len(seen_keys)}",
106
- flush=True,
107
- )
108
- return len(seen_keys)
116
+ else:
117
+ live = live_sessions()
118
+ live_ids = {s.session_id for s in live}
119
+ if live:
120
+ claude_is_responsive = True
121
+ if claude_is_responsive and session_id not in live_ids:
122
+ print(
123
+ f"\nSession ended — analysis complete. "
124
+ f"Findings detected: {len(seen_keys)}",
125
+ flush=True,
126
+ )
127
+ return len(seen_keys)
128
+ if now - idle_since >= _IDLE_TIMEOUT:
129
+ print(
130
+ f"\nSession idle for {_IDLE_TIMEOUT:.0f}s — analysis complete. "
131
+ f"Findings detected: {len(seen_keys)}",
132
+ flush=True,
133
+ )
134
+ return len(seen_keys)
109
135
 
110
136
  time.sleep(_POLL_INTERVAL)
111
137