cctx-cli 1.5.1__tar.gz → 1.7.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 (175) hide show
  1. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/CHANGELOG.md +100 -0
  2. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/PRODUCT.md +1 -0
  4. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/__init__.py +1 -1
  5. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/cli.py +36 -1
  6. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/__init__.py +54 -8
  7. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/jsonl.py +10 -0
  8. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/harvest.py +74 -4
  9. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/models.py +28 -0
  10. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/templates/autopsy.html.j2 +15 -0
  11. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/terminal.py +22 -0
  12. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +22 -6
  13. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/pyproject.toml +1 -1
  14. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/exporters/test_jsonl.py +37 -0
  15. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/renderers/test_report.py +23 -0
  16. cctx_cli-1.7.0/tests/test_diagnostician_subagents.py +298 -0
  17. cctx_cli-1.7.0/tests/test_harvest_emit.py +204 -0
  18. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.github/workflows/ci.yml +0 -0
  19. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.github/workflows/publish.yml +0 -0
  20. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.github/workflows/release.yml +0 -0
  21. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/.gitignore +0 -0
  22. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/CLAUDE.md +0 -0
  23. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/DESIGN.md +0 -0
  24. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/README.md +0 -0
  25. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/action.yml +0 -0
  26. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/agents.py +0 -0
  27. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/aggregate.py +0 -0
  28. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/inflection.py +0 -0
  29. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  30. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  31. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
  32. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  33. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  34. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  35. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  36. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/discovery.py +0 -0
  37. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/__init__.py +0 -0
  38. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/csv.py +0 -0
  39. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/exporters/json.py +0 -0
  40. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/parsers/__init__.py +0 -0
  41. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/parsers/claude_code.py +0 -0
  42. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/pricing.py +0 -0
  43. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/recommender/__init__.py +0 -0
  44. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/recommender/claude_md.py +0 -0
  45. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/recommender/evidence.py +0 -0
  46. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/__init__.py +0 -0
  47. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/github.py +0 -0
  48. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/report.py +0 -0
  49. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/renderers/trace_tui.py +0 -0
  50. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/tokenizer.py +0 -0
  51. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx/watcher.py +0 -0
  52. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/cctx-project-brief.md +0 -0
  53. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/demo.gif +0 -0
  54. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/demo.tape +0 -0
  55. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  56. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  57. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  58. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
  59. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  60. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  61. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  62. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
  63. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  64. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
  65. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  66. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  67. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  68. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  69. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  70. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
  71. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  72. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
  73. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/__init__.py +0 -0
  74. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/conftest.py +0 -0
  75. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/__init__.py +0 -0
  76. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/conftest.py +0 -0
  77. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_dead_end.py +0 -0
  78. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_inflection.py +0 -0
  79. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_orchestrator.py +0 -0
  80. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_project_specific.py +0 -0
  81. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_retry_loop.py +0 -0
  82. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_scope_creep.py +0 -0
  83. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_stale_context.py +0 -0
  84. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  85. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/exporters/__init__.py +0 -0
  86. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/exporters/test_csv.py +0 -0
  87. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/README.md +0 -0
  88. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  89. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  90. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  91. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  92. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  93. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  94. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  95. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  96. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  97. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  98. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  99. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  100. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  101. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  102. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  103. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  104. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  105. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  106. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  107. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  108. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  109. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  110. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  111. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  112. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  113. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  114. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  115. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  116. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  117. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  118. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  119. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  120. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  121. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  122. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  123. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  124. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  125. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  126. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  127. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  128. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  129. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  130. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  131. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  132. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  133. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  134. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  135. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  136. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  137. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  138. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  139. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  140. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  141. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  142. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  143. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  144. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  145. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  146. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/scrub.py +0 -0
  147. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  148. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  149. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  150. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  151. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  152. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/parsers/__init__.py +0 -0
  153. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/parsers/test_claude_code.py +0 -0
  154. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/parsers/test_claude_code_integration.py +0 -0
  155. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/recommender/__init__.py +0 -0
  156. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/recommender/test_claude_md.py +0 -0
  157. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/recommender/test_evidence.py +0 -0
  158. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/renderers/__init__.py +0 -0
  159. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  160. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_agents.py +0 -0
  161. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_aggregate.py +0 -0
  162. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_cli.py +0 -0
  163. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_cli_export.py +0 -0
  164. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_discovery.py +0 -0
  165. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_github_summary.py +0 -0
  166. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_harvest.py +0 -0
  167. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_harvest_check.py +0 -0
  168. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_models.py +0 -0
  169. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_models_project_pattern.py +0 -0
  170. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_recommender.py +0 -0
  171. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_smoke.py +0 -0
  172. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_terminal_renderer.py +0 -0
  173. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_tokenizer.py +0 -0
  174. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_trace_tui.py +0 -0
  175. {cctx_cli-1.5.1 → cctx_cli-1.7.0}/tests/test_watcher.py +0 -0
@@ -2,6 +2,106 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.7.0 (2026-06-11)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Jsonl exporter — subagent_costs in dict literal; fix import sort
10
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
11
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
12
+
13
+ - Remove unused ToolResult import in test_diagnostician_subagents
14
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
15
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
16
+
17
+ ### Documentation
18
+
19
+ - Restore billing-rate explanation in _compute_own_cost
20
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
21
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
22
+
23
+ ### Features
24
+
25
+ - Diagnostician — inclusive cost + per-subagent attribution
26
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
27
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
28
+
29
+ - HTML report + JSON exporter — subagent_costs output
30
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
31
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
32
+
33
+ - Per-subagent cost attribution in autopsy (#88)
34
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
35
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
36
+
37
+ - SubagentAttribution model + Diagnosis.subagent_costs field
38
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
39
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
40
+
41
+ - Terminal renderer — subagent cost table in autopsy output
42
+ ([#109](https://github.com/jacquardlabs/cctx/pull/109),
43
+ [`d1bc0fa`](https://github.com/jacquardlabs/cctx/commit/d1bc0fa039070ecfa671f38f5c43c864ed17e61a))
44
+
45
+
46
+ ## v1.6.0 (2026-06-10)
47
+
48
+ ### Bug Fixes
49
+
50
+ - Harvest — preview_patches dedup per (target, heading) not heading-only
51
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
52
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
53
+
54
+ - Harvest — shorten local-import comment under 100-char line limit
55
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
56
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
57
+
58
+ ### Documentation
59
+
60
+ - Harvest — correct misleading local-import comment
61
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
62
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
63
+
64
+ - Spec deviation note (sync returns patches) + PRODUCT.md cross-agent emit row
65
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
66
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
67
+
68
+ ### Features
69
+
70
+ - Cctx harvest --emit — cross-agent layer to AGENTS.md (#82)
71
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
72
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
73
+
74
+ - Cli — harvest --emit / --sync cross-agent emit
75
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
76
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
77
+
78
+ - Harvest — EMIT_TARGETS + retarget_patches (fan-out to AGENTS.md)
79
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
80
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
81
+
82
+ - Harvest — sync_managed_sections backfills CLAUDE.md into emit target
83
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
84
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
85
+
86
+ - Models — MANAGED_HEADINGS registry for cctx-owned CLAUDE.md sections
87
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
88
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
89
+
90
+ ### Testing
91
+
92
+ - Emit + sync idempotency through apply_patches
93
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
94
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
95
+
96
+ - End-to-end fan-out to both targets; spec: reconcile sync error contract
97
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
98
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
99
+
100
+ - Lock MANAGED_HEADINGS registry to recommender templates
101
+ ([#108](https://github.com/jacquardlabs/cctx/pull/108),
102
+ [`afa964c`](https://github.com/jacquardlabs/cctx/commit/afa964c68b445030e1fafe9f41c67a0de4afcd2d))
103
+
104
+
5
105
  ## v1.5.1 (2026-06-10)
6
106
 
7
107
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.5.1
3
+ Version: 1.7.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
@@ -76,6 +76,7 @@ Six commands (`ls`, `autopsy`, `harvest`, `watch`, `trace`, `export`). No comman
76
76
  | Memory-hygiene depth | `harvest --check` + `--check-severity` | v1.4.0 (M13) |
77
77
  | Live session badges | `cctx ls` | unreleased |
78
78
  | Live session detection, early idle exit | `cctx watch` | unreleased |
79
+ | Cross-agent emit | `cctx harvest --emit agents [--sync]` | M15; mirror CLAUDE.md sections to AGENTS.md — unreleased |
79
80
 
80
81
  ### Pattern classifiers (v1.4.0)
81
82
 
@@ -1,3 +1,3 @@
1
1
  """cctx: profile, debug, and optimize Claude Code and Agent SDK sessions."""
2
2
 
3
- __version__ = "1.5.1"
3
+ __version__ = "1.7.0"
@@ -23,6 +23,7 @@ from cctx.agents import live_sessions as _live_sessions
23
23
  from cctx.diagnostician import aggregate
24
24
  from cctx.diagnostician.patterns import project_specific
25
25
  from cctx.discovery import complete_project as _complete_project
26
+ from cctx.harvest import EMIT_TARGETS
26
27
  from cctx.models import KIND_LABEL, AggregateReport
27
28
  from cctx.parsers.claude_code import parse_session
28
29
  from cctx.recommender import claude_md
@@ -570,6 +571,22 @@ def trace(target: Path | None, latest: bool) -> None:
570
571
  show_default=True,
571
572
  help="Minimum severity that causes --check to exit 1.",
572
573
  )
574
+ @click.option(
575
+ "--emit",
576
+ "emit_targets",
577
+ multiple=True,
578
+ type=click.Choice(list(EMIT_TARGETS)),
579
+ help="Also write applicable patches to another agent's instruction file "
580
+ "(e.g. AGENTS.md). Repeatable.",
581
+ )
582
+ @click.option(
583
+ "--sync",
584
+ "sync_mode",
585
+ is_flag=True,
586
+ default=False,
587
+ help="With --emit: also mirror already-harvested cctx-managed sections "
588
+ "from CLAUDE.md into the emit target.",
589
+ )
573
590
  def harvest(
574
591
  target: Path,
575
592
  since: str | None,
@@ -578,9 +595,20 @@ def harvest(
578
595
  target_dir: Path | None,
579
596
  check_mode: bool,
580
597
  check_severity: str,
598
+ emit_targets: tuple[str, ...],
599
+ sync_mode: bool,
581
600
  ) -> None:
582
601
  """Apply autopsy patches to CLAUDE.md."""
583
- from cctx.harvest import apply_patches, check_claude_md, preview_patches
602
+ from cctx.harvest import (
603
+ apply_patches,
604
+ check_claude_md,
605
+ preview_patches,
606
+ retarget_patches,
607
+ sync_managed_sections,
608
+ )
609
+
610
+ if sync_mode and not emit_targets:
611
+ raise click.UsageError("--sync requires --emit.")
584
612
 
585
613
  if check_mode:
586
614
  from cctx.harvest import CheckSeverity
@@ -626,6 +654,13 @@ def harvest(
626
654
  diagnosis = claude_md.generate(diagnosis)
627
655
  patches = diagnosis.patches
628
656
 
657
+ base = patches
658
+ for t in emit_targets:
659
+ emitted = retarget_patches(base, t)
660
+ if sync_mode:
661
+ emitted = emitted + sync_managed_sections(resolved_dir, t)
662
+ patches = patches + emitted
663
+
629
664
  if not patches:
630
665
  render_harvest_results([], dry_run=dry_run)
631
666
  return
@@ -1,9 +1,9 @@
1
1
  """Autopsy diagnostician — public entry point.
2
2
 
3
3
  run(trace) -> Diagnosis
4
- Runs all three pattern classifiers, detects inflection turn,
5
- patches cost attribution for stale_context findings, and returns
6
- a Diagnosis with patches=[].
4
+ Runs all pattern classifiers, detects inflection turn,
5
+ patches stale_context cost attribution, and returns
6
+ a Diagnosis with patches=[] and subagent_costs populated.
7
7
 
8
8
  The Recommender (cctx.recommender.claude_md) populates patches.
9
9
  """
@@ -21,7 +21,7 @@ from cctx.diagnostician.patterns import (
21
21
  stale_context,
22
22
  tool_thrash,
23
23
  )
24
- from cctx.models import Diagnosis, Finding, FindingKind
24
+ from cctx.models import Diagnosis, Finding, FindingKind, SubagentAttribution
25
25
  from cctx.pricing import price_per_tok as _price_per_tok
26
26
 
27
27
  if TYPE_CHECKING:
@@ -41,8 +41,8 @@ def _patch_costs(findings: list[Finding], model: str | None) -> list[Finding]:
41
41
  return result
42
42
 
43
43
 
44
- def _compute_total_cost(trace: SessionTrace, model: str | None) -> float:
45
- """Approximate total session cost including cache reads and writes.
44
+ def _compute_own_cost(trace: SessionTrace, model: str | None) -> float:
45
+ """Parent-turns-only cost does not recurse into subagents.
46
46
 
47
47
  Billing rates relative to base input price:
48
48
  cache_read: ×0.10 (read from prompt cache)
@@ -59,6 +59,50 @@ def _compute_total_cost(trace: SessionTrace, model: str | None) -> float:
59
59
  return round(total, 4)
60
60
 
61
61
 
62
+ def _compute_inclusive_cost(trace: SessionTrace) -> float:
63
+ """Recursive cost: own turns + all subagent turns at every depth."""
64
+ own = _compute_own_cost(trace, trace.primary_model)
65
+ return own + sum(_compute_inclusive_cost(sa) for sa in trace.subagents)
66
+
67
+
68
+ def _build_label_map(trace: SessionTrace) -> dict[str, str]:
69
+ """Map child session_id → display label from the parent's Agent ToolUse inputs."""
70
+ label_map: dict[str, str] = {}
71
+ for turn in trace.turns:
72
+ for tu in turn.tool_uses:
73
+ if tu.subagent_session_id:
74
+ ti = tu.tool_input
75
+ label_map[tu.subagent_session_id] = (
76
+ ti.get("description")
77
+ or (ti.get("prompt") or "")[:80]
78
+ or tu.subagent_session_id[:12]
79
+ )
80
+ return label_map
81
+
82
+
83
+ def _collect_attributions(
84
+ trace: SessionTrace,
85
+ depth: int = 1,
86
+ label_map: dict[str, str] | None = None,
87
+ ) -> list[SubagentAttribution]:
88
+ """Flat DFS list of SubagentAttribution, one per subagent at every depth."""
89
+ if label_map is None:
90
+ label_map = _build_label_map(trace)
91
+ result: list[SubagentAttribution] = []
92
+ for child in trace.subagents:
93
+ label = label_map.get(child.session_id, child.session_id[:12])
94
+ cost = _compute_inclusive_cost(child)
95
+ result.append(SubagentAttribution(
96
+ session_id=child.session_id,
97
+ label=label,
98
+ total_cost_usd=round(cost, 4),
99
+ depth=depth,
100
+ model=child.primary_model,
101
+ ))
102
+ result.extend(_collect_attributions(child, depth + 1, None))
103
+ return result
104
+
105
+
62
106
  def run(trace: SessionTrace) -> Diagnosis:
63
107
  """Diagnose a single SessionTrace. Returns Diagnosis with patches=[]."""
64
108
  findings: list[Finding] = [
@@ -73,11 +117,12 @@ def run(trace: SessionTrace) -> Diagnosis:
73
117
  inflection_turn = inflection.detect(findings)
74
118
  findings = _patch_costs(findings, trace.primary_model)
75
119
 
76
- total_cost = _compute_total_cost(trace, trace.primary_model)
120
+ total_cost = round(_compute_inclusive_cost(trace), 4)
77
121
  waste_cost = sum(f.cost_usd for f in findings if f.cost_usd is not None)
78
- # Waste cannot exceed total session cost — cap as a logical invariant.
79
122
  waste_cost = min(waste_cost, total_cost)
80
123
 
124
+ subagent_costs = _collect_attributions(trace)
125
+
81
126
  return Diagnosis(
82
127
  session_id=trace.session_id,
83
128
  findings=findings,
@@ -86,4 +131,5 @@ def run(trace: SessionTrace) -> Diagnosis:
86
131
  total_cost_usd=total_cost,
87
132
  waste_cost_usd=round(waste_cost, 4),
88
133
  analysed_at=datetime.now(UTC),
134
+ subagent_costs=subagent_costs,
89
135
  )
@@ -50,6 +50,16 @@ def export_diagnosis(
50
50
  "patches": patches,
51
51
  "turn_count": len(trace.turns),
52
52
  "model": trace.primary_model,
53
+ "subagent_costs": [
54
+ {
55
+ "session_id": a.session_id,
56
+ "label": a.label,
57
+ "cost_usd": a.total_cost_usd,
58
+ "depth": a.depth,
59
+ "model": a.model,
60
+ }
61
+ for a in diagnosis.subagent_costs
62
+ ],
53
63
  }
54
64
  return json.dumps(obj)
55
65
 
@@ -13,6 +13,7 @@ Layering rules (MUST respect):
13
13
  """
14
14
  from __future__ import annotations
15
15
 
16
+ import dataclasses
16
17
  import re
17
18
  from collections import defaultdict
18
19
  from dataclasses import dataclass
@@ -20,6 +21,8 @@ from enum import Enum
20
21
  from pathlib import Path
21
22
  from typing import TYPE_CHECKING
22
23
 
24
+ from cctx.models import MANAGED_HEADING_PREFIX, MANAGED_HEADINGS
25
+
23
26
  if TYPE_CHECKING:
24
27
  from cctx.models import Patch
25
28
 
@@ -105,6 +108,70 @@ def _is_supported_target(patch: Patch) -> bool:
105
108
  # Public API
106
109
  # ---------------------------------------------------------------------------
107
110
 
111
+ # Maps an --emit target name to the destination filename. Single place to add
112
+ # future targets (Cursor, Windsurf, Copilot) when demand exists.
113
+ EMIT_TARGETS: dict[str, str] = {
114
+ "agents": "AGENTS.md",
115
+ }
116
+
117
+
118
+ def retarget_patches(patches: list[Patch], emit_target: str) -> list[Patch]:
119
+ """Clone CLAUDE.md-targeted patches to the emit target's file.
120
+
121
+ Only patches whose target_file is exactly "CLAUDE.md" are emitted —
122
+ .claude/rules/ and .claude/skills/ patches are Claude Code-specific and do
123
+ not translate to other agents. Returns clones; inputs are unmodified.
124
+ """
125
+ dest = EMIT_TARGETS[emit_target]
126
+ return [
127
+ dataclasses.replace(p, target_file=dest)
128
+ for p in patches
129
+ if p.target_file == "CLAUDE.md"
130
+ ]
131
+
132
+
133
+ # Reverse map: exact managed heading -> the FindingKind that owns it.
134
+ _HEADING_TO_KIND = {heading: kind for kind, heading in MANAGED_HEADINGS.items()}
135
+
136
+
137
+ def sync_managed_sections(target_dir: Path, emit_target: str) -> list[Patch]:
138
+ """Build synthetic patches mirroring CLAUDE.md's cctx-managed sections.
139
+
140
+ Reads CLAUDE.md in target_dir, keeps sections whose heading is an exact
141
+ MANAGED_HEADINGS value or starts with MANAGED_HEADING_PREFIX, and returns
142
+ one Patch per kept section targeting the emit file. Returns [] if CLAUDE.md
143
+ is absent. The CLI routes these through preview_patches / apply_patches, so
144
+ idempotency and dry-run come for free from the existing machinery.
145
+ """
146
+ from cctx.models import FindingKind, Patch # runtime use (Patch is TYPE_CHECKING-only above)
147
+
148
+ claude_md = target_dir / "CLAUDE.md"
149
+ if not claude_md.exists():
150
+ return []
151
+
152
+ dest = EMIT_TARGETS[emit_target]
153
+ content = claude_md.read_text(encoding="utf-8")
154
+ patches: list[Patch] = []
155
+
156
+ for heading, body in _parse_sections(content):
157
+ is_fixed = heading in _HEADING_TO_KIND
158
+ is_prefixed = heading.startswith(MANAGED_HEADING_PREFIX)
159
+ if not (is_fixed or is_prefixed):
160
+ continue
161
+
162
+ kind = _HEADING_TO_KIND[heading] if is_fixed else FindingKind.PROJECT_PATTERN
163
+ diff_lines = [heading] + body.splitlines()
164
+ unified_diff = "\n".join(f"+{line}" for line in diff_lines)
165
+ patches.append(Patch(
166
+ target_file=dest,
167
+ description=heading,
168
+ unified_diff=unified_diff,
169
+ finding_kind=kind,
170
+ evidence_summary="synced from CLAUDE.md",
171
+ ))
172
+
173
+ return patches
174
+
108
175
 
109
176
  def apply_patch(patch: Patch, target_dir: Path) -> ApplyResult:
110
177
  """Apply one patch. Never raises — errors go into ApplyResult(status=ERROR)."""
@@ -161,8 +228,10 @@ def apply_patch(patch: Patch, target_dir: Path) -> ApplyResult:
161
228
  def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]:
162
229
  """Compute what would happen without writing. Returns APPLIED or SKIPPED."""
163
230
  results = []
164
- # Track fingerprints already "seen" within this preview run (idempotency)
165
- seen_fingerprints: set[str] = set()
231
+ # Track (target_path, fingerprint) pairs already "seen" within this preview
232
+ # run (idempotency). Keyed by file so the same heading in two different
233
+ # target files is correctly treated as two independent patches.
234
+ seen_fingerprints: set[tuple[Path, str]] = set()
166
235
 
167
236
  for patch in patches:
168
237
  target_path = target_dir / patch.target_file
@@ -181,7 +250,8 @@ def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]
181
250
 
182
251
  content = target_path.read_text(encoding="utf-8") if target_path.exists() else ""
183
252
 
184
- if fp is not None and (_already_present(content, fp) or fp in seen_fingerprints):
253
+ already_seen = fp is not None and (target_path, fp) in seen_fingerprints
254
+ if fp is not None and (_already_present(content, fp) or already_seen):
185
255
  results.append(ApplyResult(
186
256
  patch=patch,
187
257
  status=ApplyStatus.SKIPPED,
@@ -190,7 +260,7 @@ def preview_patches(patches: list[Patch], target_dir: Path) -> list[ApplyResult]
190
260
  ))
191
261
  else:
192
262
  if fp is not None:
193
- seen_fingerprints.add(fp)
263
+ seen_fingerprints.add((target_path, fp))
194
264
  results.append(ApplyResult(
195
265
  patch=patch,
196
266
  status=ApplyStatus.APPLIED,
@@ -184,6 +184,22 @@ KIND_LABEL: dict[FindingKind, str] = {
184
184
  FindingKind.PROJECT_PATTERN: "PROJECT PATTERN",
185
185
  }
186
186
 
187
+ # Maps FindingKind to the exact ## heading emitted by its recommender patch
188
+ # template. Single source of truth for "which CLAUDE.md sections cctx owns."
189
+ # harvest.py imports this (never reaches into recommender/) so emit/sync can
190
+ # identify cctx-managed sections without depending on the patch generator.
191
+ MANAGED_HEADINGS: dict[FindingKind, str] = {
192
+ FindingKind.RETRY_LOOP: "## Retry discipline",
193
+ FindingKind.SCOPE_CREEP: "## Scope discipline",
194
+ FindingKind.STALE_CONTEXT: "## Context hygiene",
195
+ FindingKind.TOOL_THRASH: "## Tool-call discipline",
196
+ FindingKind.DEAD_END: "## Exploration discipline",
197
+ }
198
+
199
+ # Project-specific patterns use a heading that embeds tool+key, so the managed
200
+ # section is identified by prefix rather than exact match.
201
+ MANAGED_HEADING_PREFIX: str = "## Project-specific: "
202
+
187
203
 
188
204
  class Severity(str, Enum):
189
205
  HIGH = "high"
@@ -217,6 +233,17 @@ class Patch:
217
233
  evidence_summary: str
218
234
 
219
235
 
236
+ @dataclass
237
+ class SubagentAttribution:
238
+ """Cost attribution for a single subagent session."""
239
+
240
+ session_id: str
241
+ label: str # from Agent tool_input['description'], else prompt[:80]
242
+ total_cost_usd: float # inclusive: this subagent + its own children
243
+ depth: int # 1 = direct child, 2 = grandchild, …
244
+ model: str | None
245
+
246
+
220
247
  @dataclass
221
248
  class Diagnosis:
222
249
  session_id: str
@@ -226,6 +253,7 @@ class Diagnosis:
226
253
  total_cost_usd: float
227
254
  waste_cost_usd: float
228
255
  analysed_at: datetime
256
+ subagent_costs: list[SubagentAttribution] = field(default_factory=list)
229
257
 
230
258
  @property
231
259
  def verdict(self) -> str:
@@ -176,6 +176,21 @@ footer {
176
176
  <dt>Waste attributed</dt><dd>~${{ "%.2f"|format(diag.waste_cost_usd) }}</dd>
177
177
  {% if diag.inflection_turn is not none %}<dt>Inflection turn</dt><dd>{{ diag.inflection_turn }}</dd>{% endif %}
178
178
  </dl>
179
+ {% if diag.subagent_costs %}
180
+ <details class="subagent-costs">
181
+ <summary>Subagents: {{ diag.subagent_costs | selectattr("depth", "equalto", 1) | list | length }} — ${{ "%.3f" % (diag.subagent_costs | selectattr("depth", "equalto", 1) | map(attribute="total_cost_usd") | sum) }}</summary>
182
+ <table>
183
+ <tr><th>Label</th><th>Depth</th><th>Cost</th></tr>
184
+ {% for a in diag.subagent_costs %}
185
+ <tr>
186
+ <td>{{ a.label | truncate(80) }}</td>
187
+ <td>{{ a.depth }}</td>
188
+ <td>${{ "%.3f" % a.total_cost_usd }}</td>
189
+ </tr>
190
+ {% endfor %}
191
+ </table>
192
+ </details>
193
+ {% endif %}
179
194
  <p class="meta">Costs are estimates (~85–95% of actual billing; system framing not observable in JSONL)</p>
180
195
  <p class="meta">Analysed {{ diag.analysed_at.strftime("%Y-%m-%d %H:%M UTC") }}</p>
181
196
  </div>
@@ -52,7 +52,13 @@ def render_diagnosis(
52
52
  verdict = diagnosis.verdict
53
53
  verdict_style = "bold green" if not diagnosis.findings else "bold red"
54
54
  con.print(Text(f"Verdict: {verdict}", style=verdict_style))
55
+ subagent_sum = sum(a.total_cost_usd for a in diagnosis.subagent_costs if a.depth == 1)
56
+ n_sub = len([a for a in diagnosis.subagent_costs if a.depth == 1])
55
57
  cost_line = f"Session cost: ~${diagnosis.total_cost_usd:.2f}"
58
+ if n_sub:
59
+ cost_line += (
60
+ f" (includes {n_sub} subagent{'s' if n_sub != 1 else ''}: ~${subagent_sum:.2f})"
61
+ )
56
62
  if diagnosis.waste_cost_usd > 0:
57
63
  pct = (
58
64
  diagnosis.waste_cost_usd / diagnosis.total_cost_usd * 100
@@ -65,6 +71,22 @@ def render_diagnosis(
65
71
  "~85–95% of actual billing; system framing not observable in JSONL", style="dim"
66
72
  ))
67
73
 
74
+ if diagnosis.subagent_costs:
75
+ show_depth = any(a.depth > 1 for a in diagnosis.subagent_costs)
76
+ tbl = Table(show_header=True, header_style="bold", box=None, padding=(0, 1))
77
+ tbl.add_column("Subagent", no_wrap=False, max_width=48)
78
+ if show_depth:
79
+ tbl.add_column("Depth", justify="right", width=6)
80
+ tbl.add_column("Cost", justify="right", width=8)
81
+ for a in diagnosis.subagent_costs:
82
+ label = a.label if len(a.label) <= 45 else a.label[:44] + "…"
83
+ cost_cell = f"${a.total_cost_usd:.3f}"
84
+ if show_depth:
85
+ tbl.add_row(label, str(a.depth), cost_cell)
86
+ else:
87
+ tbl.add_row(label, cost_cell)
88
+ con.print(tbl)
89
+
68
90
  if not diagnosis.findings:
69
91
  con.print("\nNo findings — session looks clean.")
70
92
  return
@@ -88,12 +88,21 @@ Clones patches suitable for emission:
88
88
  reach here given the above filter, but stated explicitly for clarity).
89
89
  - Returns clones via `dataclasses.replace(patch, target_file=EMIT_TARGETS[emit_target])`.
90
90
 
91
- ### `sync_managed_sections(target_dir, emit_target) -> list[ApplyResult]`
91
+ ### `sync_managed_sections(target_dir, emit_target) -> list[Patch]`
92
+
93
+ > **Implementation deviation (2026-06-10):** This function returns
94
+ > `list[Patch]` rather than applying patches inline (the original draft returned
95
+ > `list[ApplyResult]` and called `apply_patch` itself). The CLI appends these
96
+ > patches to the same list it routes through `preview_patches` / `apply_patches`.
97
+ > Returning patches keeps `--dry-run` write-free by construction and matches the
98
+ > codebase's "CLI decides preview vs. apply" layering — applying inline could not
99
+ > preview, contradicting the `--dry-run` requirement and `test_dry_run_no_writes`.
92
100
 
93
101
  1. Reads `CLAUDE.md` from `target_dir`. Returns empty list if absent.
94
102
  2. Calls `_parse_sections(content)` (already in `harvest.py`).
95
103
  3. Keeps sections whose heading is exactly in `MANAGED_HEADINGS.values()` OR
96
- starts with `MANAGED_HEADING_PREFIX`.
104
+ starts with `MANAGED_HEADING_PREFIX`. The leading `("(preamble)", …)` pair
105
+ matches neither branch and is skipped.
97
106
  4. For each kept section, constructs a synthetic `Patch` with:
98
107
  - `target_file = EMIT_TARGETS[emit_target]`
99
108
  - `unified_diff = "\n".join(f"+{line}" for line in [heading] + body.splitlines())`
@@ -102,8 +111,10 @@ Clones patches suitable for emission:
102
111
  `FindingKind.PROJECT_PATTERN` for `## Project-specific: …` prefixed headings
103
112
  - `description = heading`
104
113
  - `evidence_summary = "synced from CLAUDE.md"`
105
- 5. Applies each synthetic patch via `apply_patch`, which handles idempotency
106
- via `_already_present` (the `## Heading` line is the fingerprint).
114
+ 5. Returns the list of synthetic patches. The CLI routes them through the
115
+ existing `preview_patches` / `apply_patches` machinery, which handles
116
+ idempotency via `_already_present` (the `## Heading` line is the fingerprint)
117
+ and dry-run preview without writing.
107
118
 
108
119
  ---
109
120
 
@@ -143,8 +154,13 @@ under their full path.
143
154
  ## Error contract
144
155
 
145
156
  - Never raises. All failures return `ApplyResult(status=ERROR, message=...)`.
146
- - `--sync` with no CLAUDE.md: returns `[]` (not an error), logged as a
147
- single `SKIPPED` line: "CLAUDE.md not found nothing to sync."
157
+ - `--sync` with no CLAUDE.md: `sync_managed_sections` returns `[]` (not an
158
+ error). Because it returns patches rather than applying inline (see the
159
+ deviation note above), there is no `SKIPPED` line for the missing file — the
160
+ empty result simply contributes nothing, and if no other patches exist the CLI
161
+ prints its standard "No patches to apply." message. (The original draft
162
+ emitted a "CLAUDE.md not found — nothing to sync." line; that belonged to the
163
+ inline-apply design and no longer applies.)
148
164
  - Emit target directory is created by `apply_patch`'s existing `parent.mkdir`.
149
165
 
150
166
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cctx-cli"
7
- version = "1.5.1"
7
+ version = "1.7.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"
@@ -229,6 +229,43 @@ def test_write_produces_one_line_per_session() -> None:
229
229
  assert ids == {"sess-001", "sess-002"}
230
230
 
231
231
 
232
+ def test_export_diagnosis_includes_subagent_costs() -> None:
233
+ """JSON export includes subagent_costs array with correct fields."""
234
+ import dataclasses
235
+ import json
236
+
237
+ from cctx.exporters.jsonl import export_diagnosis
238
+ from cctx.models import SubagentAttribution
239
+
240
+ diag = _make_diagnosis()
241
+ trace = _make_trace()
242
+ diag = dataclasses.replace(diag, subagent_costs=[
243
+ SubagentAttribution(
244
+ session_id="child-1",
245
+ label="My task",
246
+ total_cost_usd=0.020,
247
+ depth=1,
248
+ model="claude-sonnet-4",
249
+ )
250
+ ])
251
+ data = json.loads(export_diagnosis(diag, trace))
252
+ assert "subagent_costs" in data
253
+ assert len(data["subagent_costs"]) == 1
254
+ assert data["subagent_costs"][0]["session_id"] == "child-1"
255
+ assert data["subagent_costs"][0]["cost_usd"] == pytest.approx(0.020)
256
+ assert data["subagent_costs"][0]["depth"] == 1
257
+
258
+
259
+ def test_export_diagnosis_subagent_costs_empty_by_default() -> None:
260
+ """JSON export has subagent_costs: [] when no subagents."""
261
+ from cctx.exporters.jsonl import export_diagnosis
262
+
263
+ diag = _make_diagnosis()
264
+ trace = _make_trace()
265
+ data = json.loads(export_diagnosis(diag, trace))
266
+ assert data["subagent_costs"] == []
267
+
268
+
232
269
  def test_write_empty_list_produces_no_output() -> None:
233
270
  """write() with an empty list produces no output."""
234
271
  from cctx.exporters.jsonl import write