cctx-cli 1.11.0__tar.gz → 1.12.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 (187) hide show
  1. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/CHANGELOG.md +64 -0
  2. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/PKG-INFO +1 -1
  3. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/__init__.py +1 -1
  4. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/cli.py +52 -1
  5. cctx_cli-1.12.0/cctx/parsers/otel.py +346 -0
  6. cctx_cli-1.12.0/docs/quickstart-openai-agents.md +106 -0
  7. cctx_cli-1.12.0/docs/superpowers/plans/2026-06-19-otel-parser.md +1215 -0
  8. cctx_cli-1.12.0/docs/superpowers/specs/2026-06-19-otel-parser-design.md +115 -0
  9. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/pyproject.toml +1 -1
  10. cctx_cli-1.12.0/tests/fixtures/otel_fanout.jsonl +1 -0
  11. cctx_cli-1.12.0/tests/fixtures/otel_handoff.jsonl +1 -0
  12. cctx_cli-1.12.0/tests/test_otel_parser.py +350 -0
  13. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.github/workflows/ci.yml +0 -0
  14. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.github/workflows/publish.yml +0 -0
  15. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.github/workflows/release.yml +0 -0
  16. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.gitignore +0 -0
  17. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/CLAUDE.md +0 -0
  18. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/DESIGN.md +0 -0
  19. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/PRODUCT.md +0 -0
  20. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/README.md +0 -0
  21. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/action.yml +0 -0
  22. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/agents.py +0 -0
  23. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/__init__.py +0 -0
  24. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/aggregate.py +0 -0
  25. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/inflection.py +0 -0
  26. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/__init__.py +0 -0
  27. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
  28. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/fan_out.py +0 -0
  29. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
  30. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
  31. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
  32. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
  33. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
  34. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/discovery.py +0 -0
  35. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/__init__.py +0 -0
  36. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/csv.py +0 -0
  37. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/json.py +0 -0
  38. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/jsonl.py +0 -0
  39. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/harvest.py +0 -0
  40. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/hook_installer.py +0 -0
  41. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/models.py +0 -0
  42. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/parsers/__init__.py +0 -0
  43. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/parsers/claude_code.py +0 -0
  44. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/pricing.py +0 -0
  45. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/recommender/__init__.py +0 -0
  46. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/recommender/claude_md.py +0 -0
  47. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/recommender/evidence.py +0 -0
  48. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/__init__.py +0 -0
  49. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/github.py +0 -0
  50. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/report.py +0 -0
  51. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
  52. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/terminal.py +0 -0
  53. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/trace_tui.py +0 -0
  54. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/tokenizer.py +0 -0
  55. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/watcher.py +0 -0
  56. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx-project-brief.md +0 -0
  57. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/demo.gif +0 -0
  58. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/demo.tape +0 -0
  59. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
  60. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
  61. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
  62. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
  63. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
  64. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
  65. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
  66. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
  67. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
  68. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
  69. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
  70. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
  71. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
  72. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
  73. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
  74. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
  75. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
  76. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
  77. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
  78. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/__init__.py +0 -0
  79. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/conftest.py +0 -0
  80. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/__init__.py +0 -0
  81. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/conftest.py +0 -0
  82. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_dead_end.py +0 -0
  83. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_inflection.py +0 -0
  84. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_orchestrator.py +0 -0
  85. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_project_specific.py +0 -0
  86. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_retry_loop.py +0 -0
  87. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_scope_creep.py +0 -0
  88. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_stale_context.py +0 -0
  89. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_tool_thrash.py +0 -0
  90. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/exporters/__init__.py +0 -0
  91. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/exporters/test_csv.py +0 -0
  92. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/exporters/test_jsonl.py +0 -0
  93. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/README.md +0 -0
  94. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
  95. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
  96. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
  97. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
  98. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
  99. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
  100. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
  101. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
  102. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
  103. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
  104. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
  105. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
  106. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
  107. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
  108. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
  109. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
  110. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
  111. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
  112. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
  113. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
  114. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
  115. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
  116. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
  117. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
  118. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
  119. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
  120. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
  121. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
  122. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
  123. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
  124. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
  125. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
  126. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
  127. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
  128. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
  129. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
  130. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
  131. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
  132. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
  133. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
  134. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
  135. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
  136. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
  137. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
  138. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
  139. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
  140. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
  141. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
  142. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
  143. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
  144. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
  145. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
  146. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
  147. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
  148. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
  149. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
  150. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
  151. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
  152. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/scrub.py +0 -0
  153. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
  154. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
  155. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
  156. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
  157. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
  158. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/parsers/__init__.py +0 -0
  159. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/parsers/test_claude_code.py +0 -0
  160. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/parsers/test_claude_code_integration.py +0 -0
  161. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/recommender/__init__.py +0 -0
  162. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/recommender/test_claude_md.py +0 -0
  163. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/recommender/test_evidence.py +0 -0
  164. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/renderers/__init__.py +0 -0
  165. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/renderers/test_report.py +0 -0
  166. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
  167. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_agents.py +0 -0
  168. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_aggregate.py +0 -0
  169. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_cli.py +0 -0
  170. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_cli_export.py +0 -0
  171. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_diagnostician_subagents.py +0 -0
  172. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_discovery.py +0 -0
  173. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_efficacy.py +0 -0
  174. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_fanout_classifier.py +0 -0
  175. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_github_summary.py +0 -0
  176. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_harvest.py +0 -0
  177. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_harvest_check.py +0 -0
  178. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_harvest_emit.py +0 -0
  179. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_init.py +0 -0
  180. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_models.py +0 -0
  181. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_models_project_pattern.py +0 -0
  182. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_recommender.py +0 -0
  183. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_smoke.py +0 -0
  184. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_terminal_renderer.py +0 -0
  185. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_tokenizer.py +0 -0
  186. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_trace_tui.py +0 -0
  187. {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_watcher.py +0 -0
@@ -2,6 +2,70 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.12.0 (2026-06-20)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Otel parser — per-trace warnings, multiple-root warning, full span list for subagent turns
10
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
11
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
12
+
13
+ - Ruff F401 + I001 in test_otel_parser.py ([#116](https://github.com/jacquardlabs/cctx/pull/116),
14
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
15
+
16
+ ### Documentation
17
+
18
+ - OTEL parser design spec — OpenAI Agents SDK support via parsers/otel.py
19
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
20
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
21
+
22
+ - OTEL parser implementation plan ([#116](https://github.com/jacquardlabs/cctx/pull/116),
23
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
24
+
25
+ - Quickstart guide for OpenAI Agents SDK + cctx OTEL integration
26
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
27
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
28
+
29
+ ### Features
30
+
31
+ - _detect_source() — auto-detect Claude Code vs OTEL trace format
32
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
33
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
34
+
35
+ - OpenAI Agents SDK support via OTEL parser ([#116](https://github.com/jacquardlabs/cctx/pull/116),
36
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
37
+
38
+ - OTEL parser skeleton — span loading and session trace construction
39
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
40
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
41
+
42
+ - Wire OTEL auto-detection into autopsy — cctx autopsy <otel.jsonl> just works
43
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
44
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
45
+
46
+ ### Testing
47
+
48
+ - Add OTLP JSONL fixtures for otel parser (handoff + fanout)
49
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
50
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
51
+
52
+ - OTEL parser error handling — malformed JSON, unknown spans, empty file
53
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
54
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
55
+
56
+ - Verify child AgentSpan → subagents mapping (handoff + fan-out)
57
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
58
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
59
+
60
+ - Verify FunctionSpan → ToolUse + ToolResult mapping
61
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
62
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
63
+
64
+ - Verify GenerationSpan → Turn mapping for OTEL parser
65
+ ([#116](https://github.com/jacquardlabs/cctx/pull/116),
66
+ [`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
67
+
68
+
5
69
  ## v1.11.0 (2026-06-11)
6
70
 
7
71
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cctx-cli
3
- Version: 1.11.0
3
+ Version: 1.12.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.11.0"
3
+ __version__ = "1.12.0"
@@ -13,6 +13,7 @@ Commands:
13
13
  """
14
14
  from __future__ import annotations
15
15
 
16
+ import json as _json
16
17
  from datetime import datetime, timedelta, timezone
17
18
  from pathlib import Path
18
19
  from typing import IO
@@ -179,6 +180,48 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
179
180
  con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
180
181
 
181
182
 
183
+ _CLAUDE_CODE_LINE_TYPES = frozenset({
184
+ "user", "assistant", "system", "attachment",
185
+ "last-prompt", "permission-mode", "ai-title", "custom-title",
186
+ "queue-operation", "file-history-snapshot", "pr-link",
187
+ })
188
+
189
+
190
+ def _detect_source(path: Path) -> str:
191
+ """Sniff first non-empty lines to detect trace format.
192
+
193
+ Returns "claude_code" or "otel".
194
+ Raises click.UsageError if the format cannot be determined.
195
+ """
196
+ try:
197
+ with path.open(encoding="utf-8", errors="replace") as f:
198
+ for _ in range(5):
199
+ line = f.readline()
200
+ if not line:
201
+ break
202
+ line = line.strip()
203
+ if not line:
204
+ continue
205
+ try:
206
+ obj = _json.loads(line)
207
+ except _json.JSONDecodeError:
208
+ continue
209
+ if "resourceSpans" in obj:
210
+ return "otel"
211
+ if "traceId" in obj and "spanId" in obj:
212
+ return "otel"
213
+ line_type = obj.get("type")
214
+ if isinstance(line_type, str) and line_type in _CLAUDE_CODE_LINE_TYPES:
215
+ return "claude_code"
216
+ except OSError as exc:
217
+ raise click.UsageError(f"Cannot read file: {path}: {exc}") from exc
218
+
219
+ raise click.UsageError(
220
+ f"Cannot determine trace format for {path}.\n"
221
+ "Expected a Claude Code JSONL session file or an OTLP JSON trace export."
222
+ )
223
+
224
+
182
225
  @click.group()
183
226
  def cli() -> None:
184
227
  """cctx — find out why your Claude Code session went sideways."""
@@ -415,7 +458,15 @@ def autopsy(
415
458
  "TARGET is a directory. Use --since N for cross-session mode, "
416
459
  "or pass a .jsonl file directly."
417
460
  )
418
- trace = tokenize_session(parse_session(target))
461
+ source = _detect_source(target)
462
+ if source == "otel":
463
+ from cctx.parsers.otel import parse_otel_file as _parse_otel_file
464
+ otel_traces = _parse_otel_file(target)
465
+ if not otel_traces:
466
+ raise click.UsageError(f"No traces found in {target}")
467
+ trace = tokenize_session(otel_traces[0])
468
+ else:
469
+ trace = tokenize_session(parse_session(target))
419
470
  diagnosis = diagnostician.run(trace)
420
471
  diagnosis = claude_md.generate(diagnosis)
421
472
  if quiet:
@@ -0,0 +1,346 @@
1
+ """OTLP JSONL parser for OpenAI Agents SDK traces.
2
+
3
+ Public API:
4
+ parse_otel_file(path: Path) -> list[SessionTrace]
5
+
6
+ Reads OTLP JSON (one ResourceSpans batch per line). Maps spans to the
7
+ cctx canonical model. No imports from anthropic, click, or other cctx
8
+ modules except cctx.models.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from collections import defaultdict
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ from cctx.models import (
18
+ ParserWarning,
19
+ SessionTrace,
20
+ ToolResult,
21
+ ToolUse,
22
+ Turn,
23
+ Usage,
24
+ )
25
+
26
+
27
+ def parse_otel_file(path: Path) -> list[SessionTrace]:
28
+ """Read an OTLP JSONL export; return one SessionTrace per trace_id."""
29
+ path = Path(path)
30
+ load_warnings: list[ParserWarning] = []
31
+ spans = _load_spans(path, load_warnings)
32
+
33
+ by_trace: dict[str, list[dict]] = defaultdict(list)
34
+ for span in spans:
35
+ trace_id = span.get("traceId", "")
36
+ if trace_id:
37
+ by_trace[trace_id].append(span)
38
+
39
+ traces = []
40
+ for trace_id, trace_spans in by_trace.items():
41
+ # Each trace gets its own warnings list; load-time warnings are copied in.
42
+ trace_warnings = list(load_warnings)
43
+ traces.append(_build_session_trace(trace_id, trace_spans, path, trace_warnings))
44
+ return traces
45
+
46
+
47
+ def _load_spans(path: Path, warnings: list[ParserWarning]) -> list[dict]:
48
+ """Load all spans from OTLP JSONL. One ResourceSpans JSON object per line."""
49
+ spans: list[dict] = []
50
+ try:
51
+ with path.open(encoding="utf-8", errors="replace") as f:
52
+ for lineno, line in enumerate(f, 1):
53
+ line = line.strip()
54
+ if not line:
55
+ continue
56
+ try:
57
+ obj = json.loads(line)
58
+ except json.JSONDecodeError:
59
+ warnings.append(
60
+ ParserWarning(
61
+ code="malformed_json",
62
+ detail=f"skipped malformed JSON on line {lineno}",
63
+ line_number=lineno,
64
+ path=path,
65
+ )
66
+ )
67
+ continue
68
+ for rs in obj.get("resourceSpans", []):
69
+ for ss in rs.get("scopeSpans", []):
70
+ for span in ss.get("spans", []):
71
+ span["_resource"] = rs.get("resource", {})
72
+ spans.append(span)
73
+ except OSError as exc:
74
+ warnings.append(
75
+ ParserWarning(code="read_error", detail=str(exc), path=path)
76
+ )
77
+ return spans
78
+
79
+
80
+ def _build_session_trace(
81
+ trace_id: str,
82
+ spans: list[dict],
83
+ source_path: Path,
84
+ warnings: list[ParserWarning],
85
+ ) -> SessionTrace:
86
+ root = _find_root_agent_span(spans, warnings)
87
+ if root is None:
88
+ root = {}
89
+
90
+ turns = _build_turns(root, spans)
91
+ subagents = _build_subagents(root, spans, source_path, warnings)
92
+ primary_model = _primary_model(spans)
93
+ cwd = _attr_str(root, "process.cwd") or ""
94
+
95
+ all_start_times = [
96
+ _nano_to_dt(s.get("startTimeUnixNano"))
97
+ for s in spans
98
+ if s.get("startTimeUnixNano")
99
+ ]
100
+ start_time = min((t for t in all_start_times if t), default=None)
101
+ all_end_times = [
102
+ _nano_to_dt(s.get("endTimeUnixNano"))
103
+ for s in spans
104
+ if s.get("endTimeUnixNano")
105
+ ]
106
+ end_time = max((t for t in all_end_times if t), default=None)
107
+
108
+ tool_names: list[str] = list({
109
+ name
110
+ for s in spans
111
+ if _attr_str(s, "gen_ai.operation.name") == "invoke_function"
112
+ if (name := _attr_str(s, "gen_ai.tool.name"))
113
+ })
114
+
115
+ return SessionTrace(
116
+ session_id=trace_id,
117
+ parent_session_id=None,
118
+ project_path="",
119
+ cwd=cwd,
120
+ primary_model=primary_model,
121
+ claude_code_version=None,
122
+ turns=turns,
123
+ subagents=subagents,
124
+ attachments=[],
125
+ raw_tool_result_files=[],
126
+ initial_context_tokens=0,
127
+ tool_names_loaded=tool_names,
128
+ start_time=start_time,
129
+ end_time=end_time,
130
+ source_path=source_path,
131
+ subagent_meta={},
132
+ warnings=warnings,
133
+ subagent_parse_errors=[],
134
+ )
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Span tree helpers
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ def _find_root_agent_span(
143
+ spans: list[dict], warnings: list[ParserWarning]
144
+ ) -> dict | None:
145
+ roots = [
146
+ s for s in spans
147
+ if _attr_str(s, "gen_ai.operation.name") == "run_agent"
148
+ and not s.get("parentSpanId")
149
+ ]
150
+ if not roots:
151
+ warnings.append(
152
+ ParserWarning(code="no_root_agent_span", detail="no root AgentSpan found")
153
+ )
154
+ return None
155
+ if len(roots) > 1:
156
+ span_ids = ", ".join(r.get("spanId", "?") for r in roots)
157
+ warnings.append(
158
+ ParserWarning(
159
+ code="multiple_root_agent_spans",
160
+ detail=f"found {len(roots)} root AgentSpans; using first: {span_ids}",
161
+ )
162
+ )
163
+ return roots[0]
164
+
165
+
166
+ def _build_turns(agent_span: dict, all_spans: list[dict]) -> list[Turn]:
167
+ """Build assistant turns from GenerationSpans that are direct children of agent_span."""
168
+ if not agent_span:
169
+ return []
170
+ agent_id = agent_span.get("spanId", "")
171
+ gen_spans = [
172
+ s for s in all_spans
173
+ if s.get("parentSpanId") == agent_id
174
+ and _attr_str(s, "gen_ai.operation.name") == "chat"
175
+ ]
176
+ gen_spans.sort(key=lambda s: int(s.get("startTimeUnixNano") or 0))
177
+
178
+ turns: list[Turn] = []
179
+ for i, gs in enumerate(gen_spans, 1):
180
+ tool_uses, tool_results = _build_tool_uses(gs, all_spans)
181
+ usage = Usage(
182
+ input_tokens=_attr_int(gs, "gen_ai.usage.input_tokens") or 0,
183
+ output_tokens=_attr_int(gs, "gen_ai.usage.output_tokens") or 0,
184
+ cache_creation_5m=0,
185
+ cache_creation_1h=0,
186
+ cache_read=0,
187
+ service_tier=None,
188
+ )
189
+ ts = _nano_to_dt(gs.get("startTimeUnixNano")) or datetime.now(timezone.utc)
190
+ end_ns = gs.get("endTimeUnixNano")
191
+ start_ns = gs.get("startTimeUnixNano")
192
+ duration_ms: int | None = None
193
+ if end_ns and start_ns:
194
+ duration_ms = int((int(end_ns) - int(start_ns)) / 1_000_000)
195
+
196
+ turns.append(
197
+ Turn(
198
+ turn_number=i,
199
+ uuid=gs.get("spanId", ""),
200
+ parent_uuid=agent_id or None,
201
+ role="assistant",
202
+ text=_attr_str(gs, "gen_ai.output.text") or "",
203
+ thinking="",
204
+ tool_uses=tool_uses,
205
+ tool_results=tool_results,
206
+ usage=usage,
207
+ model=_attr_str(gs, "gen_ai.request.model"),
208
+ stop_reason="end_turn",
209
+ timestamp=ts,
210
+ duration_ms=duration_ms,
211
+ )
212
+ )
213
+ return turns
214
+
215
+
216
+ def _build_tool_uses(
217
+ gen_span: dict, all_spans: list[dict]
218
+ ) -> tuple[list[ToolUse], list[ToolResult]]:
219
+ """Extract ToolUse + ToolResult from FunctionSpan children of a GenerationSpan."""
220
+ gen_id = gen_span.get("spanId", "")
221
+ func_spans = [
222
+ s for s in all_spans
223
+ if s.get("parentSpanId") == gen_id
224
+ and _attr_str(s, "gen_ai.operation.name") == "invoke_function"
225
+ ]
226
+ func_spans.sort(key=lambda s: int(s.get("startTimeUnixNano") or 0))
227
+
228
+ tool_uses: list[ToolUse] = []
229
+ tool_results: list[ToolResult] = []
230
+ for fs in func_spans:
231
+ tool_name = _attr_str(fs, "gen_ai.tool.name") or "unknown"
232
+ call_id = _attr_str(fs, "gen_ai.tool.call.id") or fs.get("spanId", "")
233
+ result_str = _attr_str(fs, "gen_ai.tool.call.result") or ""
234
+ is_error = fs.get("status", {}).get("code", 1) == 2 # OTEL STATUS_ERROR = 2
235
+
236
+ tool_uses.append(ToolUse(tool_name=tool_name, tool_use_id=call_id, tool_input={}))
237
+ tool_results.append(
238
+ ToolResult(
239
+ tool_name=tool_name,
240
+ tool_use_id=call_id,
241
+ content=result_str,
242
+ structured=None,
243
+ is_error=is_error,
244
+ )
245
+ )
246
+ return tool_uses, tool_results
247
+
248
+
249
+ def _build_subagents(
250
+ root_span: dict,
251
+ all_spans: list[dict],
252
+ source_path: Path,
253
+ warnings: list[ParserWarning],
254
+ ) -> list[SessionTrace]:
255
+ """Build child SessionTraces from child AgentSpans (handoffs + parallel sub-agents)."""
256
+ if not root_span:
257
+ return []
258
+ root_id = root_span.get("spanId", "")
259
+ child_agent_spans = [
260
+ s for s in all_spans
261
+ if s.get("parentSpanId") == root_id
262
+ and _attr_str(s, "gen_ai.operation.name") == "run_agent"
263
+ ]
264
+ child_agent_spans.sort(key=lambda s: int(s.get("startTimeUnixNano") or 0))
265
+
266
+ subagents: list[SessionTrace] = []
267
+ for child in child_agent_spans:
268
+ child_id = child.get("spanId", "")
269
+ # child_spans: direct children only (used for primary_model / time bounds).
270
+ # _build_turns receives the full all_spans list so _build_tool_uses can
271
+ # locate FunctionSpan grandchildren regardless of nesting depth.
272
+ child_spans = [s for s in all_spans if s.get("parentSpanId") == child_id]
273
+ agent_name = _attr_str(child, "gen_ai.agent.name") or child_id
274
+ child_turns = _build_turns(child, all_spans)
275
+ child_start = _nano_to_dt(child.get("startTimeUnixNano"))
276
+ child_end = _nano_to_dt(child.get("endTimeUnixNano"))
277
+
278
+ subagents.append(
279
+ SessionTrace(
280
+ session_id=child_id,
281
+ parent_session_id=root_id or None,
282
+ project_path="",
283
+ cwd="",
284
+ primary_model=_primary_model(child_spans),
285
+ claude_code_version=None,
286
+ turns=child_turns,
287
+ subagents=[],
288
+ attachments=[],
289
+ raw_tool_result_files=[],
290
+ initial_context_tokens=0,
291
+ tool_names_loaded=[],
292
+ start_time=child_start,
293
+ end_time=child_end,
294
+ source_path=source_path,
295
+ subagent_meta={"agent_name": agent_name},
296
+ warnings=[],
297
+ subagent_parse_errors=[],
298
+ )
299
+ )
300
+ return subagents
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # Attribute + time helpers
305
+ # ---------------------------------------------------------------------------
306
+
307
+
308
+ def _primary_model(spans: list[dict]) -> str | None:
309
+ models = [
310
+ m for s in spans
311
+ if _attr_str(s, "gen_ai.operation.name") == "chat"
312
+ if (m := _attr_str(s, "gen_ai.request.model"))
313
+ ]
314
+ if not models:
315
+ return None
316
+ return max(set(models), key=models.count)
317
+
318
+
319
+ def _attr_str(span: dict, key: str) -> str | None:
320
+ for attr in span.get("attributes", []):
321
+ if attr.get("key") == key:
322
+ return attr.get("value", {}).get("stringValue")
323
+ return None
324
+
325
+
326
+ def _attr_int(span: dict, key: str) -> int | None:
327
+ for attr in span.get("attributes", []):
328
+ if attr.get("key") == key:
329
+ val = attr.get("value", {})
330
+ raw = val.get("intValue") or val.get("doubleValue")
331
+ if raw is not None:
332
+ try:
333
+ return int(float(raw))
334
+ except (ValueError, TypeError):
335
+ return None
336
+ return None
337
+
338
+
339
+ def _nano_to_dt(nano: str | int | None) -> datetime | None:
340
+ if nano is None:
341
+ return None
342
+ try:
343
+ ns = int(nano)
344
+ return datetime.fromtimestamp(ns / 1e9, tz=timezone.utc)
345
+ except (ValueError, TypeError, OSError):
346
+ return None
@@ -0,0 +1,106 @@
1
+ # Using cctx with the OpenAI Agents SDK
2
+
3
+ cctx diagnoses OpenAI Agents SDK runs the same way it diagnoses Claude Code sessions — point it at a trace file and get an autopsy.
4
+
5
+ ## 1. Install dependencies
6
+
7
+ ```bash
8
+ pip install cctx opentelemetry-sdk
9
+ ```
10
+
11
+ ## 2. Export traces to a local file
12
+
13
+ Add this to your agent script before running your agent. It configures OpenTelemetry to write spans to a local JSONL file.
14
+
15
+ ```python
16
+ import json
17
+ from typing import Sequence
18
+
19
+ from opentelemetry.sdk.trace import TracerProvider
20
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter, SpanExportResult
21
+ from opentelemetry.sdk.trace.export import ReadableSpan
22
+
23
+
24
+ class FileSpanExporter(SpanExporter):
25
+ """Writes OTLP-style JSON batches to a local file, one line per flush."""
26
+
27
+ def __init__(self, path: str) -> None:
28
+ self._path = path
29
+
30
+ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
31
+ batch = {
32
+ "resourceSpans": [{
33
+ "resource": {"attributes": []},
34
+ "scopeSpans": [{
35
+ "scope": {"name": "openai.agents"},
36
+ "spans": [self._span_to_dict(s) for s in spans],
37
+ }],
38
+ }]
39
+ }
40
+ with open(self._path, "a", encoding="utf-8") as f:
41
+ f.write(json.dumps(batch) + "\n")
42
+ return SpanExportResult.SUCCESS
43
+
44
+ def shutdown(self) -> None:
45
+ pass
46
+
47
+ @staticmethod
48
+ def _span_to_dict(span: ReadableSpan) -> dict:
49
+ ctx = span.context
50
+ attrs = [
51
+ {"key": k, "value": _otlp_value(v)}
52
+ for k, v in (span.attributes or {}).items()
53
+ ]
54
+ d: dict = {
55
+ "traceId": format(ctx.trace_id, "032x"),
56
+ "spanId": format(ctx.span_id, "016x"),
57
+ "name": span.name,
58
+ "kind": int(span.kind),
59
+ "startTimeUnixNano": str(span.start_time),
60
+ "endTimeUnixNano": str(span.end_time),
61
+ "attributes": attrs,
62
+ "status": {"code": span.status.status_code.value},
63
+ }
64
+ if span.parent is not None:
65
+ d["parentSpanId"] = format(span.parent.span_id, "016x")
66
+ return d
67
+
68
+
69
+ def _otlp_value(v: object) -> dict:
70
+ if isinstance(v, bool):
71
+ return {"boolValue": v}
72
+ if isinstance(v, int):
73
+ return {"intValue": str(v)}
74
+ if isinstance(v, float):
75
+ return {"doubleValue": v}
76
+ return {"stringValue": str(v)}
77
+
78
+
79
+ provider = TracerProvider()
80
+ provider.add_span_processor(BatchSpanProcessor(FileSpanExporter("agent_trace.jsonl")))
81
+
82
+ # Wire into the OpenAI Agents SDK — exact API depends on your SDK version:
83
+ # from agents import set_trace_processors
84
+ # set_trace_processors([...])
85
+ ```
86
+
87
+ ## 3. Run your agent
88
+
89
+ ```bash
90
+ python my_agent.py
91
+ # agent_trace.jsonl is written
92
+ ```
93
+
94
+ ## 4. Diagnose the run
95
+
96
+ ```bash
97
+ cctx autopsy agent_trace.jsonl
98
+ ```
99
+
100
+ `cctx autopsy` picks up the OTLP format automatically — no flags needed.
101
+
102
+ ## Notes
103
+
104
+ - If your trace file contains multiple runs, cctx diagnoses the first trace by trace ID.
105
+ - Token costs shown in the autopsy are estimates; cctx does not call the OpenAI API.
106
+ - The exact `set_trace_processors` API varies by SDK version — check the `openai-agents` changelog if the import above doesn't work.