cctx-cli 1.3.0__tar.gz → 1.5.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 (172) hide show
  1. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/CHANGELOG.md +101 -0
  2. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/__init__.py +1 -1
  4. cctx_cli-1.5.0/cctx/agents.py +66 -0
  5. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/cli.py +42 -9
  6. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/harvest.py +187 -18
  7. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/terminal.py +35 -4
  8. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/watcher.py +37 -11
  9. cctx_cli-1.5.0/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +860 -0
  10. cctx_cli-1.5.0/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +839 -0
  11. cctx_cli-1.5.0/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +214 -0
  12. cctx_cli-1.5.0/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +128 -0
  13. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/pyproject.toml +1 -1
  14. cctx_cli-1.5.0/tests/test_agents.py +128 -0
  15. cctx_cli-1.5.0/tests/test_harvest_check.py +418 -0
  16. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_terminal_renderer.py +154 -0
  17. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_watcher.py +101 -0
  18. cctx_cli-1.3.0/tests/test_harvest_check.py +0 -139
  19. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.github/workflows/ci.yml +0 -0
  20. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.github/workflows/publish.yml +0 -0
  21. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.github/workflows/release.yml +0 -0
  22. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/.gitignore +0 -0
  23. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/CLAUDE.md +0 -0
  24. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/DESIGN.md +0 -0
  25. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/PRODUCT.md +0 -0
  26. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/README.md +0 -0
  27. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/action.yml +0 -0
  28. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/__init__.py +0 -0
  29. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/aggregate.py +0 -0
  30. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/inflection.py +0 -0
  31. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  32. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  33. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
  34. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  35. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  36. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  37. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  38. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/discovery.py +0 -0
  39. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/__init__.py +0 -0
  40. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/csv.py +0 -0
  41. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/json.py +0 -0
  42. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/exporters/jsonl.py +0 -0
  43. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/models.py +0 -0
  44. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/parsers/__init__.py +0 -0
  45. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/parsers/claude_code.py +0 -0
  46. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/pricing.py +0 -0
  47. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/recommender/__init__.py +0 -0
  48. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/recommender/claude_md.py +0 -0
  49. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/recommender/evidence.py +0 -0
  50. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/__init__.py +0 -0
  51. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/github.py +0 -0
  52. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/report.py +0 -0
  53. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  54. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/renderers/trace_tui.py +0 -0
  55. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx/tokenizer.py +0 -0
  56. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/cctx-project-brief.md +0 -0
  57. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/demo.gif +0 -0
  58. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/demo.tape +0 -0
  59. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  60. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  61. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  62. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  63. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  64. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  65. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  66. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  67. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  68. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  69. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  70. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  71. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  72. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/__init__.py +0 -0
  73. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/conftest.py +0 -0
  74. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/__init__.py +0 -0
  75. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/conftest.py +0 -0
  76. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_dead_end.py +0 -0
  77. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_inflection.py +0 -0
  78. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_orchestrator.py +0 -0
  79. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_project_specific.py +0 -0
  80. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_retry_loop.py +0 -0
  81. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_scope_creep.py +0 -0
  82. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_stale_context.py +0 -0
  83. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  84. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/exporters/__init__.py +0 -0
  85. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/exporters/test_csv.py +0 -0
  86. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/exporters/test_jsonl.py +0 -0
  87. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/README.md +0 -0
  88. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  89. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  90. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  91. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  92. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  93. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  94. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  95. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  96. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  97. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  98. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  99. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  100. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  101. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  102. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  103. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  104. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  105. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  106. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  107. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  108. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  109. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  110. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  111. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  112. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  113. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  114. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  115. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  116. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  117. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  118. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  119. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  120. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  121. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  122. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  123. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  124. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  125. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  126. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  127. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  128. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  129. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  130. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  131. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  132. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  133. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  134. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  135. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  136. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  137. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  138. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  139. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  140. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  141. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  142. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  143. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  144. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  145. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  146. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/scrub.py +0 -0
  147. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  148. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  149. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  150. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  151. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  152. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/parsers/__init__.py +0 -0
  153. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/parsers/test_claude_code.py +0 -0
  154. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/parsers/test_claude_code_integration.py +0 -0
  155. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/recommender/__init__.py +0 -0
  156. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/recommender/test_claude_md.py +0 -0
  157. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/recommender/test_evidence.py +0 -0
  158. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/renderers/__init__.py +0 -0
  159. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/renderers/test_report.py +0 -0
  160. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  161. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_aggregate.py +0 -0
  162. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_cli.py +0 -0
  163. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_cli_export.py +0 -0
  164. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_discovery.py +0 -0
  165. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_github_summary.py +0 -0
  166. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_harvest.py +0 -0
  167. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_models.py +0 -0
  168. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_models_project_pattern.py +0 -0
  169. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_recommender.py +0 -0
  170. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_smoke.py +0 -0
  171. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_tokenizer.py +0 -0
  172. {cctx_cli-1.3.0 → cctx_cli-1.5.0}/tests/test_trace_tui.py +0 -0
@@ -2,6 +2,107 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.5.0 (2026-05-20)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Agents.py — guard against non-list JSON, tighten patch targets
10
+ ([`c41c42c`](https://github.com/jacquardlabs/cctx/commit/c41c42cb366904fc332638b8f97ee042f17b45c2))
11
+
12
+ - Renderer — guard order, tmp_path fixtures, missing no-badge tests
13
+ ([`8f26afd`](https://github.com/jacquardlabs/cctx/commit/8f26afdf27c63641942b434e47a2fb7b24d38068))
14
+
15
+ - Watcher — hermetic tests, reuse _encode_path, rename clarity
16
+ ([`a7b52de`](https://github.com/jacquardlabs/cctx/commit/a7b52def47b13447d956476a0ec6782fe2d0247b))
17
+
18
+ ### Documentation
19
+
20
+ - Implementation plan for claude agents live integration
21
+ ([`2136adf`](https://github.com/jacquardlabs/cctx/commit/2136adf0697a695d27f438f0368e7bd5ba406e89))
22
+
23
+ - Spec for claude agents --json live session integration
24
+ ([`0df2381`](https://github.com/jacquardlabs/cctx/commit/0df23813a90c66e57d0d39c6b959859b89a5c057))
25
+
26
+ ### Features
27
+
28
+ - Add agents.py — live_sessions() via claude agents --json
29
+ ([`83b704f`](https://github.com/jacquardlabs/cctx/commit/83b704ffbe4303dbd316257a01eb0b59307c0e06))
30
+
31
+ - Cctx ls — pass live_statuses to renderer for live session badges
32
+ ([`65445d7`](https://github.com/jacquardlabs/cctx/commit/65445d75a213bf26401fe044a2409d2ce0efbcb1))
33
+
34
+ - Render_sessions/render_projects — live status badges via live_statuses param
35
+ ([`3d77687`](https://github.com/jacquardlabs/cctx/commit/3d776879c9ce03a622361c2b804e5986d40f37a1))
36
+
37
+ - Watcher — live session detection + early idle exit via claude agents --json
38
+ ([`a11481c`](https://github.com/jacquardlabs/cctx/commit/a11481c2126fead87e1f92bfecc7bf5ac3f39d1a))
39
+
40
+
41
+ ## v1.4.0 (2026-05-20)
42
+
43
+ ### Bug Fixes
44
+
45
+ - Deduplicate harvest check import, align severity badge output
46
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
47
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
48
+
49
+ - Move defaultdict import to top-level, add _words() return type
50
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
51
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
52
+
53
+ - Use removeprefix instead of lstrip to preserve .claude/skills/ dot prefix
54
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
55
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
56
+
57
+ ### Documentation
58
+
59
+ - M15 harvest --check depth design spec ([#87](https://github.com/jacquardlabs/cctx/pull/87),
60
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
61
+
62
+ - M15 harvest --check depth implementation plan
63
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
64
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
65
+
66
+ ### Features
67
+
68
+ - --check-severity flag and severity badges in harvest --check output
69
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
70
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
71
+
72
+ - Check_contradictions() — always/never keyword heuristic
73
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
74
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
75
+
76
+ - Check_redundancy() — Jaccard similarity ≥ 0.8 on section word sets
77
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
78
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
79
+
80
+ - Check_staleness() — backtick function refs grepped against project source
81
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
82
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
83
+
84
+ - CheckSeverity enum, severity field on CheckFinding, new CheckIssue values
85
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
86
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
87
+
88
+ - Harvest --check depth — contradiction, redundancy, staleness detectors + --check-severity
89
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
90
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
91
+
92
+ - Wire all four detectors into check_claude_md ([#87](https://github.com/jacquardlabs/cctx/pull/87),
93
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
94
+
95
+ ### Refactoring
96
+
97
+ - Check_redundancy — compute _words once per section, remove dead union guard
98
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
99
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
100
+
101
+ - Check_staleness — module-level _STALENESS_EXCLUDED, min-len in regex, per-file search
102
+ ([#87](https://github.com/jacquardlabs/cctx/pull/87),
103
+ [`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
104
+
105
+
5
106
  ## v1.3.0 (2026-05-17)
6
107
 
7
108
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.3.0
3
+ Version: 1.5.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.3.0"
3
+ __version__ = "1.5.0"
@@ -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
@@ -147,22 +148,32 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
147
148
  from rich.console import Console
148
149
  from rich.rule import Rule
149
150
 
151
+ from cctx.harvest import CheckIssue, CheckSeverity
152
+
150
153
  con = Console()
151
154
  claude_md_path = target_dir / "CLAUDE.md"
152
155
  con.print(Rule(f"cctx harvest --check — {claude_md_path}"))
153
156
  if not findings:
154
- con.print("✓ CLAUDE.md looks clean — no dead references or empty sections.")
157
+ con.print("✓ CLAUDE.md looks clean — no issues found.")
155
158
  return
156
159
  con.print(f"{len(findings)} issue(s) found:\n")
157
- from cctx.harvest import CheckIssue
158
160
  _ISSUE_LABEL = {
159
- CheckIssue.DEAD_FILE_REF: "dead file reference",
160
- CheckIssue.DEAD_SKILL_REF: "dead skill reference",
161
- CheckIssue.EMPTY_SECTION: "empty section",
161
+ CheckIssue.DEAD_FILE_REF: "dead file reference",
162
+ CheckIssue.DEAD_SKILL_REF: "dead skill reference",
163
+ CheckIssue.EMPTY_SECTION: "empty section",
164
+ CheckIssue.CONTRADICTION: "contradiction",
165
+ CheckIssue.REDUNDANCY: "redundancy",
166
+ CheckIssue.STALE_IDENTIFIER: "stale identifier",
167
+ }
168
+ _SEV_BADGE = {
169
+ CheckSeverity.HIGH: "[HIGH]",
170
+ CheckSeverity.MEDIUM: "[MED]",
171
+ CheckSeverity.LOW: "[LOW]",
162
172
  }
163
173
  for f in findings:
174
+ badge = _SEV_BADGE.get(f.severity, " ")
164
175
  label = _ISSUE_LABEL.get(f.issue, f.issue.value)
165
- con.print(f" [{f.heading}] {label}: {f.detail}")
176
+ con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
166
177
 
167
178
 
168
179
  @click.group()
@@ -186,9 +197,11 @@ def ls(project: Path | None) -> None:
186
197
  """
187
198
  from cctx.discovery import ProjectInfo, find_project_dir, list_projects, list_sessions
188
199
 
200
+ live_statuses = {s.session_id: s.status for s in _live_sessions()}
201
+
189
202
  if project is None:
190
203
  projects = list_projects()
191
- render_projects(projects)
204
+ render_projects(projects, live_statuses=live_statuses)
192
205
  else:
193
206
  cwd = project if project.is_dir() else project.parent
194
207
  project_dir = find_project_dir(cwd)
@@ -203,7 +216,7 @@ def ls(project: Path | None) -> None:
203
216
  display_name=str(cwd).replace(str(Path.home()), "~"),
204
217
  sessions=sessions,
205
218
  )
206
- render_sessions(info)
219
+ render_sessions(info, live_statuses=live_statuses)
207
220
 
208
221
 
209
222
  @cli.command()
@@ -549,6 +562,14 @@ def trace(target: Path | None, latest: bool) -> None:
549
562
  default=False,
550
563
  help="Audit existing CLAUDE.md for dead references and empty sections. Exit 1 if findings.",
551
564
  )
565
+ @click.option(
566
+ "--check-severity",
567
+ "check_severity",
568
+ default="MEDIUM",
569
+ type=click.Choice(["LOW", "MEDIUM", "HIGH"], case_sensitive=False),
570
+ show_default=True,
571
+ help="Minimum severity that causes --check to exit 1.",
572
+ )
552
573
  def harvest(
553
574
  target: Path,
554
575
  since: str | None,
@@ -556,15 +577,27 @@ def harvest(
556
577
  dry_run: bool,
557
578
  target_dir: Path | None,
558
579
  check_mode: bool,
580
+ check_severity: str,
559
581
  ) -> None:
560
582
  """Apply autopsy patches to CLAUDE.md."""
561
583
  from cctx.harvest import apply_patches, check_claude_md, preview_patches
562
584
 
563
585
  if check_mode:
586
+ from cctx.harvest import CheckSeverity
564
587
  resolved_dir = target_dir or Path.cwd()
565
588
  findings = check_claude_md(resolved_dir)
566
589
  _render_check_findings(findings, resolved_dir)
567
- raise SystemExit(1 if findings else 0)
590
+ _SEVERITY_ORDER = {
591
+ CheckSeverity.LOW: 0,
592
+ CheckSeverity.MEDIUM: 1,
593
+ CheckSeverity.HIGH: 2,
594
+ }
595
+ threshold = CheckSeverity(check_severity.lower())
596
+ triggering = [
597
+ f for f in findings
598
+ if _SEVERITY_ORDER[f.severity] >= _SEVERITY_ORDER[threshold]
599
+ ]
600
+ raise SystemExit(1 if triggering else 0)
568
601
 
569
602
  if apply_mode and dry_run:
570
603
  raise click.UsageError("--apply and --dry-run are mutually exclusive.")
@@ -14,6 +14,7 @@ Layering rules (MUST respect):
14
14
  from __future__ import annotations
15
15
 
16
16
  import re
17
+ from collections import defaultdict
17
18
  from dataclasses import dataclass
18
19
  from enum import Enum
19
20
  from pathlib import Path
@@ -28,17 +29,27 @@ if TYPE_CHECKING:
28
29
  # ---------------------------------------------------------------------------
29
30
 
30
31
 
32
+ class CheckSeverity(str, Enum):
33
+ LOW = "low"
34
+ MEDIUM = "medium"
35
+ HIGH = "high"
36
+
37
+
31
38
  class CheckIssue(str, Enum):
32
- DEAD_FILE_REF = "dead_file_ref" # backtick-quoted path that doesn't exist on disk
33
- DEAD_SKILL_REF = "dead_skill_ref" # .claude/skills/ reference that doesn't exist
34
- EMPTY_SECTION = "empty_section" # ## heading with no content
39
+ DEAD_FILE_REF = "dead_file_ref"
40
+ DEAD_SKILL_REF = "dead_skill_ref"
41
+ EMPTY_SECTION = "empty_section"
42
+ CONTRADICTION = "contradiction"
43
+ REDUNDANCY = "redundancy"
44
+ STALE_IDENTIFIER = "stale_identifier"
35
45
 
36
46
 
37
47
  @dataclass
38
48
  class CheckFinding:
39
- heading: str # ## section where this was found ("(preamble)" if before first heading)
40
- issue: CheckIssue
41
- detail: str # human-readable description
49
+ heading: str
50
+ issue: CheckIssue
51
+ severity: CheckSeverity
52
+ detail: str
42
53
 
43
54
 
44
55
  class ApplyStatus(str, Enum):
@@ -212,6 +223,25 @@ _KNOWN_EXTENSIONS = {
212
223
  ".json", ".md", ".sh", ".bash", ".fish", ".zsh",
213
224
  }
214
225
 
226
+ _STOPWORDS = {
227
+ "a", "an", "the", "to", "be", "is", "are", "was", "were",
228
+ "in", "on", "at", "of", "for", "with", "and", "or", "not",
229
+ "it", "this", "that", "you", "your", "use", "do",
230
+ }
231
+
232
+ _ALWAYS_NEVER_RE = re.compile(
233
+ r"\b(always|never)\b(.+?)(?:[.!?\n]|$)", re.IGNORECASE
234
+ )
235
+
236
+ _STALENESS_EXCLUDED = {".git", ".venv", "node_modules", "__pycache__"}
237
+
238
+ _FUNC_REF_RE = re.compile(r"`([^`/.\s]{8,})\(\)`")
239
+
240
+
241
+ def _words(text: str) -> set[str]:
242
+ tokens = re.findall(r"\b[a-zA-Z_]\w*\b", text.lower())
243
+ return {t for t in tokens if t not in _STOPWORDS}
244
+
215
245
 
216
246
  def _parse_sections(content: str) -> list[tuple[str, str]]:
217
247
  """Split markdown into (heading, body) pairs.
@@ -233,22 +263,133 @@ def _parse_sections(content: str) -> list[tuple[str, str]]:
233
263
  return sections
234
264
 
235
265
 
236
- def check_claude_md(target_dir: Path) -> list[CheckFinding]:
237
- """Audit CLAUDE.md in target_dir for deterministically detectable issues.
266
+ def check_contradictions(
267
+ sections: list[tuple[str, str]],
268
+ ) -> list[CheckFinding]:
269
+ """Detect contradictions across sections using always/never polarity heuristic.
238
270
 
239
- Checks:
240
- - Dead file references: backtick-quoted paths that don't exist on disk
241
- - Dead skill references: .claude/skills/ paths that don't exist
242
- - Empty sections: ## headings with no content
271
+ Looks for "always" and "never" clauses in section bodies, extracts the
272
+ subject words, and flags cases where the same word has conflicting polarities.
243
273
 
244
- Returns an empty list if CLAUDE.md doesn't exist (not an error).
274
+ Returns findings for each contradiction found (severity: HIGH).
245
275
  """
246
- claude_md = target_dir / "CLAUDE.md"
247
- if not claude_md.exists():
276
+ subject_map: dict[str, list[tuple[str, str]]] = defaultdict(list)
277
+ for heading, body in sections:
278
+ for match in _ALWAYS_NEVER_RE.finditer(body):
279
+ polarity = match.group(1).lower()
280
+ clause = match.group(2)
281
+ for word in _words(clause):
282
+ subject_map[word].append((polarity, heading))
283
+
284
+ findings: list[CheckFinding] = []
285
+ seen: set[tuple[str, str]] = set()
286
+ for word, occurrences in subject_map.items():
287
+ always_headings = [h for p, h in occurrences if p == "always"]
288
+ never_headings = [h for p, h in occurrences if p == "never"]
289
+ if always_headings and never_headings:
290
+ key = (always_headings[0], never_headings[0])
291
+ if key not in seen:
292
+ seen.add(key)
293
+ findings.append(CheckFinding(
294
+ heading=always_headings[0],
295
+ issue=CheckIssue.CONTRADICTION,
296
+ severity=CheckSeverity.HIGH,
297
+ detail=(
298
+ f"'{word}' is 'always' in {always_headings[0]!r}"
299
+ f" but 'never' in {never_headings[0]!r}"
300
+ ),
301
+ ))
302
+ return findings
303
+
304
+
305
+ def check_redundancy(
306
+ sections: list[tuple[str, str]],
307
+ ) -> list[CheckFinding]:
308
+ """Detect redundancy across sections using Jaccard similarity.
309
+
310
+ Builds a word set (stopwords removed) for each section. Sections with
311
+ fewer than 5 words are ineligible. For all pairs of eligible sections,
312
+ computes Jaccard similarity of their word sets. Flags pairs with
313
+ similarity >= 0.8.
314
+
315
+ Returns findings for each redundancy found (severity: MEDIUM).
316
+ """
317
+ eligible = []
318
+ for heading, body in sections:
319
+ ws = _words(body)
320
+ if len(ws) >= 5:
321
+ eligible.append((heading, body, ws))
322
+
323
+ findings: list[CheckFinding] = []
324
+ for i in range(len(eligible)):
325
+ for j in range(i + 1, len(eligible)):
326
+ h1, _, w1 = eligible[i]
327
+ h2, _, w2 = eligible[j]
328
+ union = w1 | w2
329
+ jaccard = len(w1 & w2) / len(union)
330
+ if jaccard >= 0.8:
331
+ findings.append(CheckFinding(
332
+ heading=h1,
333
+ issue=CheckIssue.REDUNDANCY,
334
+ severity=CheckSeverity.MEDIUM,
335
+ detail=f"{h1!r} and {h2!r} are {jaccard:.0%} similar",
336
+ ))
337
+ return findings
338
+
339
+
340
+ def check_staleness(
341
+ sections: list[tuple[str, str]],
342
+ project_dir: Path,
343
+ ) -> list[CheckFinding]:
344
+ """Detect stale function references in CLAUDE.md.
345
+
346
+ Scans all .py, .ts, and .js source files in the project directory and
347
+ searches for backtick-quoted function references (e.g., `my_function()`)
348
+ that are 8+ characters long. Flags references not found in the source.
349
+
350
+ Returns findings for each stale identifier found (severity: LOW).
351
+ """
352
+ source_files = [
353
+ f
354
+ for f in (
355
+ list(project_dir.rglob("*.py"))
356
+ + list(project_dir.rglob("*.ts"))
357
+ + list(project_dir.rglob("*.js"))
358
+ )
359
+ if not any(part in _STALENESS_EXCLUDED for part in f.parts)
360
+ ]
361
+ if not source_files:
248
362
  return []
249
363
 
250
- content = claude_md.read_text(encoding="utf-8")
251
- sections = _parse_sections(content)
364
+ findings: list[CheckFinding] = []
365
+ for heading, body in sections:
366
+ for match in _FUNC_REF_RE.finditer(body):
367
+ name = match.group(1)
368
+ found = any(
369
+ name in f.read_text(encoding="utf-8", errors="ignore")
370
+ for f in source_files
371
+ )
372
+ if not found:
373
+ findings.append(CheckFinding(
374
+ heading=heading,
375
+ issue=CheckIssue.STALE_IDENTIFIER,
376
+ severity=CheckSeverity.LOW,
377
+ detail=f"'{name}()' not found in project source files",
378
+ ))
379
+ return findings
380
+
381
+
382
+ def _check_structure(
383
+ sections: list[tuple[str, str]],
384
+ target_dir: Path,
385
+ ) -> list[CheckFinding]:
386
+ """Check structure issues: empty sections, dead file/skill references.
387
+
388
+ Returns findings for:
389
+ - Empty sections: ## headings with no content (MEDIUM)
390
+ - Dead file references: backtick-quoted paths that don't exist (MEDIUM)
391
+ - Dead skill references: .claude/skills/ paths that don't exist (MEDIUM)
392
+ """
252
393
  findings: list[CheckFinding] = []
253
394
 
254
395
  for heading, body in sections:
@@ -259,13 +400,14 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
259
400
  findings.append(CheckFinding(
260
401
  heading=heading,
261
402
  issue=CheckIssue.EMPTY_SECTION,
403
+ severity=CheckSeverity.MEDIUM,
262
404
  detail=f"{heading!r} has no content",
263
405
  ))
264
406
  continue
265
407
 
266
408
  # Dead skill references
267
409
  for match in _SKILL_REF_RE.finditer(body):
268
- skill_path_str = match.group(1).lstrip("./")
410
+ skill_path_str = match.group(1).removeprefix("./")
269
411
  # Try resolving from target_dir and from home
270
412
  candidates = [
271
413
  target_dir / skill_path_str,
@@ -275,6 +417,7 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
275
417
  findings.append(CheckFinding(
276
418
  heading=heading,
277
419
  issue=CheckIssue.DEAD_SKILL_REF,
420
+ severity=CheckSeverity.MEDIUM,
278
421
  detail=f"skill not found: {match.group(1)!r}",
279
422
  ))
280
423
 
@@ -292,7 +435,33 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
292
435
  findings.append(CheckFinding(
293
436
  heading=heading,
294
437
  issue=CheckIssue.DEAD_FILE_REF,
438
+ severity=CheckSeverity.MEDIUM,
295
439
  detail=f"file not found: {token!r}",
296
440
  ))
297
441
 
298
442
  return findings
443
+
444
+
445
+ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
446
+ """Audit CLAUDE.md in target_dir for deterministically detectable issues.
447
+
448
+ Checks:
449
+ - Dead file/skill references and empty sections (MEDIUM)
450
+ - Contradictory always/never rules (HIGH)
451
+ - Redundant sections with Jaccard >= 0.8 (MEDIUM)
452
+ - Stale backtick-quoted function identifiers >= 8 chars (LOW)
453
+
454
+ Returns an empty list if CLAUDE.md doesn't exist (not an error).
455
+ """
456
+ claude_md = target_dir / "CLAUDE.md"
457
+ if not claude_md.exists():
458
+ return []
459
+
460
+ content = claude_md.read_text(encoding="utf-8")
461
+ sections = _parse_sections(content)
462
+ return (
463
+ _check_structure(sections, target_dir)
464
+ + check_contradictions(sections)
465
+ + check_redundancy(sections)
466
+ + check_staleness(sections, target_dir)
467
+ )
@@ -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()