claude-code-conductor 2.20.0__tar.gz → 2.22.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 (195) hide show
  1. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/select_tier.py +9 -0
  2. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/session_stop.py +17 -0
  3. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/scripts/record_tier_outcome.py +5 -1
  4. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/CHANGELOG.md +60 -0
  5. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/PKG-INFO +1 -1
  6. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/__init__.py +1 -1
  7. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_tier.py +44 -1
  8. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/db.py +352 -3
  9. claude_code_conductor-2.22.0/src/c3/migrations/002_agent_cost_runs.sql +41 -0
  10. claude_code_conductor-2.22.0/src/c3/migrations/003_tier_cost.sql +25 -0
  11. claude_code_conductor-2.22.0/src/c3/pricing.py +157 -0
  12. claude_code_conductor-2.22.0/src/c3/usage_ingester.py +388 -0
  13. claude_code_conductor-2.22.0/tests/fixtures/usage/README.md +18 -0
  14. claude_code_conductor-2.22.0/tests/fixtures/usage/mainline.jsonl +5 -0
  15. claude_code_conductor-2.22.0/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +3 -0
  16. claude_code_conductor-2.22.0/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +1 -0
  17. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_record_tier_outcome.py +115 -0
  18. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_select_tier.py +82 -0
  19. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_session_start.py +11 -5
  20. claude_code_conductor-2.22.0/tests/test_cli_tier.py +507 -0
  21. claude_code_conductor-2.22.0/tests/test_db.py +518 -0
  22. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_migrate.py +215 -0
  23. claude_code_conductor-2.22.0/tests/test_pricing.py +225 -0
  24. claude_code_conductor-2.22.0/tests/test_usage_ingester.py +529 -0
  25. claude_code_conductor-2.20.0/tests/test_cli_tier.py +0 -258
  26. claude_code_conductor-2.20.0/tests/test_db.py +0 -166
  27. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/CLAUDE.md +0 -0
  28. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/architect.md +0 -0
  29. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/code-reviewer.md +0 -0
  30. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/developer.md +0 -0
  31. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/doc-writer.md +0 -0
  32. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/interviewer.md +0 -0
  33. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/planner.md +0 -0
  34. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/project-setup.md +0 -0
  35. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/security-reviewer.md +0 -0
  36. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/systematic-debugger.md +0 -0
  37. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/tester.md +0 -0
  38. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/wt_developer.md +0 -0
  39. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/wt_systematic-debugger.md +0 -0
  40. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/wt_tester.md +0 -0
  41. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/breaking-changes.txt +0 -0
  42. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/deletions.txt +0 -0
  43. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/config-policy.md +0 -0
  44. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/parallel-agents-setup.md +0 -0
  45. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/platform-adapters.md +0 -0
  46. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/settings.json.md +0 -0
  47. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/_hook_utils.py +0 -0
  48. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/check_agent_invocation.py +0 -0
  49. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/consolidate_memory.py +0 -0
  50. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/permission_handler.py +0 -0
  51. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/permission_handler_toast.py +0 -0
  52. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/planner_check.py +0 -0
  53. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/post_tool.py +0 -0
  54. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/pre_compact.py +0 -0
  55. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/pre_tool.py +0 -0
  56. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/recall_inject.py +0 -0
  57. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/restore_session.py +0 -0
  58. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/session_start.py +0 -0
  59. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/session_utils.py +0 -0
  60. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/statusline.py +0 -0
  61. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/stop.py +0 -0
  62. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/worktree_guard.py +0 -0
  63. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/memory/.gitkeep +0 -0
  64. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/permission_rules.json +0 -0
  65. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/rules/promoted/index.md +0 -0
  66. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/settings.json +0 -0
  67. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/codex-review/SKILL.md +0 -0
  68. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/SKILL.md +0 -0
  69. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
  70. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
  71. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
  72. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +0 -0
  73. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
  74. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/develop/SKILL.md +0 -0
  75. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/doc/SKILL.md +0 -0
  76. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/extract-lib/SKILL.md +0 -0
  77. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/init-session/SKILL.md +0 -0
  78. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/mcp-config/SKILL.md +0 -0
  79. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/parallel-agents/SKILL.md +0 -0
  80. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/pattern-status/SKILL.md +0 -0
  81. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
  82. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/recall/SKILL.md +0 -0
  83. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
  84. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
  85. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/review-phase/SKILL.md +0 -0
  86. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/SKILL.md +0 -0
  87. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/reference.md +0 -0
  88. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
  89. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
  90. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/start/SKILL.md +0 -0
  91. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/state/.gitkeep +0 -0
  92. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.gitignore +0 -0
  93. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSE +0 -0
  94. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/chroma-hnswlib-LICENSE +0 -0
  95. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/chroma-hnswlib-NOTICE +0 -0
  96. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/fastembed-LICENSE +0 -0
  97. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/fastembed-NOTICE +0 -0
  98. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/onnxruntime-LICENSE +0 -0
  99. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
  100. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/README.md +0 -0
  101. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/hatch_build.py +0 -0
  102. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/pyproject.toml +0 -0
  103. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/__main__.py +0 -0
  104. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/_excludes.py +0 -0
  105. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/_terminal.py +0 -0
  106. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/adapters.py +0 -0
  107. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli.py +0 -0
  108. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_ask.py +0 -0
  109. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_doctor.py +0 -0
  110. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_init.py +0 -0
  111. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_list.py +0 -0
  112. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_plan.py +0 -0
  113. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_recall.py +0 -0
  114. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_update.py +0 -0
  115. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/embedding.py +0 -0
  116. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/mcp_server.py +0 -0
  117. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrate.py +0 -0
  118. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrations/001_initial.sql +0 -0
  119. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrations/README.md +0 -0
  120. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrations/__init__.py +0 -0
  121. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/paths.py +0 -0
  122. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/plan_validator.py +0 -0
  123. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/platforms.py +0 -0
  124. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/question.py +0 -0
  125. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/recall_chunker.py +0 -0
  126. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/recall_index.py +0 -0
  127. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/__init__.py +0 -0
  128. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/conftest.py +0 -0
  129. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/__init__.py +0 -0
  130. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_check_agent_invocation.py +0 -0
  131. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_consolidate_memory.py +0 -0
  132. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_hook_utils.py +0 -0
  133. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_permission_handler.py +0 -0
  134. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_permission_handler_toast.py +0 -0
  135. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
  136. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_planner_check.py +0 -0
  137. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_planner_check_dev.py +0 -0
  138. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_post_tool.py +0 -0
  139. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_pre_tool.py +0 -0
  140. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_recall_inject.py +0 -0
  141. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_record_review_decision.py +0 -0
  142. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_restore_session.py +0 -0
  143. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_review_hint_inject.py +0 -0
  144. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_select_tier_escalation.py +0 -0
  145. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_session_stop.py +0 -0
  146. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_session_utils.py +0 -0
  147. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
  148. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_similarity_boost.py +0 -0
  149. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_statusline.py +0 -0
  150. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_statusline_template_sync.py +0 -0
  151. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_sync_check.py +0 -0
  152. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_template_guard.py +0 -0
  153. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/__init__.py +0 -0
  154. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/_skill_helpers.py +0 -0
  155. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
  156. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_init_session_no_task_type.py +0 -0
  157. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_planner_lightweight.py +0 -0
  158. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_recall_skill.py +0 -0
  159. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
  160. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_setup_templates.py +0 -0
  161. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
  162. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_start_skill_new_flow.py +0 -0
  163. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
  164. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_adapters.py +0 -0
  165. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_ask.py +0 -0
  166. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_entry.py +0 -0
  167. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_init.py +0 -0
  168. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_list.py +0 -0
  169. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_plan.py +0 -0
  170. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_recall.py +0 -0
  171. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_update_breaking_changes.py +0 -0
  172. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_update_deletions.py +0 -0
  173. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_docstring_consistency.py +0 -0
  174. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_embedding.py +0 -0
  175. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_excludes.py +0 -0
  176. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_extract_breaking_changes.py +0 -0
  177. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_mcp_server_elicit.py +0 -0
  178. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_paths.py +0 -0
  179. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_plan_validator.py +0 -0
  180. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_pre_compact.py +0 -0
  181. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_pre_tool_hook.py +0 -0
  182. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_precompact_additional.py +0 -0
  183. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_precompact_toctou_fixes.py +0 -0
  184. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_recall_chunker.py +0 -0
  185. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_recall_index.py +0 -0
  186. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_references_migration.py +0 -0
  187. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_session_utils_additional.py +0 -0
  188. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_skill_no_builtin_conflict.py +0 -0
  189. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_statusline.py +0 -0
  190. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_stop_additional.py +0 -0
  191. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_stop_hook.py +0 -0
  192. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_stop_precompact_fixes.py +0 -0
  193. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_sync_template_stop.py +0 -0
  194. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_template_pre_tool_hook.py +0 -0
  195. {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_worktree_guard.py +0 -0
@@ -323,6 +323,7 @@ def write_tier_selection(
323
323
  escalation_reason: str | None = None,
324
324
  prompt_prefix: str | None = None,
325
325
  prompt_hash: str | None = None,
326
+ session_id: str | None = None,
326
327
  ) -> None:
327
328
  """直近の選択結果を ``tier_selection.json`` に書く。
328
329
 
@@ -335,6 +336,9 @@ def write_tier_selection(
335
336
 
336
337
  ``escalated`` / ``escalation_reason`` を任意で含める。
337
338
  failure rate に基づく昇格が起きた場合のみ True / 文字列が入る。
339
+
340
+ ``session_id`` を任意で含める。UserPromptSubmit payload の session UUID。
341
+ None のときは tier_selection.json のキー自体を省略する(後方互換)。
338
342
  """
339
343
  os.makedirs(os.path.dirname(TIER_SELECTION_PATH), exist_ok=True)
340
344
  payload: dict[str, object] = {
@@ -354,6 +358,8 @@ def write_tier_selection(
354
358
  payload["prompt_prefix"] = prompt_prefix
355
359
  if prompt_hash is not None:
356
360
  payload["prompt_hash"] = prompt_hash
361
+ if session_id is not None:
362
+ payload["session_id"] = session_id
357
363
  try:
358
364
  with open(TIER_SELECTION_PATH, "w", encoding="utf-8") as f:
359
365
  json.dump(payload, f, ensure_ascii=False)
@@ -415,6 +421,8 @@ def main() -> int:
415
421
  if not isinstance(prompt, str) or not prompt.strip():
416
422
  return 0
417
423
 
424
+ session_id = payload.get("session_id")
425
+
418
426
  # Phase 2-C: 類似度推定で complexity を補強
419
427
  heuristic_complexity = estimate_complexity(prompt)
420
428
  strong_complexity, weak_matches = similarity_boost(prompt)
@@ -448,6 +456,7 @@ def main() -> int:
448
456
  escalated=escalated, escalation_reason=escalation_reason,
449
457
  prompt_prefix=prompt_prefix,
450
458
  prompt_hash=prompt_hash,
459
+ session_id=session_id,
451
460
  )
452
461
 
453
462
  context_text = build_additional_context(
@@ -89,6 +89,23 @@ def main() -> int:
89
89
  except Exception as e:
90
90
  print(f"[session_stop:consolidate_memory] failed: {e}", file=sys.stderr)
91
91
 
92
+ # Phase 3: usage_ingester — セッションログからコスト集計データを ingestion
93
+ # worktree session では起動しない(stop.run の is_worktree 判定と整合)
94
+ # 安全側動作: session_utils ロード失敗時や is_worktree 判定失敗時は ingester をスキップする。
95
+ # worktree 判定不能で誤起動するより skip を優先(except Exception で捕捉 → 早期終了)。
96
+ try:
97
+ session_utils_module = _load_module("session_utils")
98
+ if not session_utils_module.is_worktree(os.getcwd()):
99
+ session_id = payload.get("session_id")
100
+ transcript_path = payload.get("transcript_path")
101
+ if session_id and transcript_path:
102
+ from pathlib import Path as _Path # noqa: PLC0415
103
+ from c3.usage_ingester import ingest_session # noqa: PLC0415
104
+ project_dir = _Path(transcript_path).parent
105
+ ingest_session(session_id=session_id, project_dir=project_dir)
106
+ except Exception as e:
107
+ print(f"[session_stop:usage_ingester] failed: {type(e).__name__}", file=sys.stderr)
108
+
92
109
  return 0
93
110
 
94
111
 
@@ -210,15 +210,19 @@ def main(argv: list[str] | None = None) -> int:
210
210
  # Phase 2-B: tier_recent_outcomes にも 1 件記録(escalation 判定の母数)。
211
211
  # tier_bandit 更新が失敗してもこちらは試みる(DB が一時的に詰まる程度なら
212
212
  # 片方だけ通る可能性もある)。
213
+ # v2.22.0: session_id を渡して cost 紐づけを可能にする。
214
+ # session_id キーが無い古い tier_selection.json の場合は None になる(後方互換)。
215
+ session_id = selection.get("session_id")
213
216
  try:
214
217
  c3_db.record_tier_recent_outcome(
215
218
  complexity=selection["complexity"],
216
219
  tier=selection["tier"],
217
220
  success=success,
221
+ session_id=session_id,
218
222
  )
219
223
  except Exception as exc: # noqa: BLE001
220
224
  print(
221
- f"[record_tier_outcome] tier_recent_outcomes record skipped: {exc}",
225
+ f"[record_tier_outcome] tier_recent_outcomes record skipped: {type(exc).__name__}",
222
226
  file=sys.stderr,
223
227
  )
224
228
 
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.22.0] - 2026-05-25
4
+
5
+ **tier-routing cost 紐づけデータ蓄積**: tier_recent_outcomes に session_id 列を追加し、agent_cost_runs と JOIN できるデータ基盤を整備する。`c3 tier stats` に complexity×tier 別の平均コストセクションを追加する。cost-aware routing 本体は v2.23.0。
6
+
7
+ **スコープ注記**: 単価テーブル自動更新は公式 price API が非提供のため pricing.py の手動メンテ継続。tier_bandit の total_cost_usd / cost_samples 列は v2.23.0 用に確保のみ(書き込み・読み出しなし)。
8
+
9
+ ### 機能追加
10
+
11
+ - **`src/c3/migrations/003_tier_cost.sql`(新規)**: `tier_recent_outcomes` に `session_id TEXT` 列と `idx_tier_recent_session` インデックスを追加。`tier_bandit` に `total_cost_usd REAL DEFAULT 0.0` / `cost_samples INTEGER DEFAULT 0` 列を追加(v2.23.0 用確保のみ)。既存データは ADD COLUMN DEFAULT で保持(破壊的変更なし)。
12
+
13
+ - **`src/c3/db.py`: `record_tier_recent_outcome` に `session_id` 追加**: kw-only 引数 `session_id: str | None = None` を追加。既存呼び出しは後方互換(省略時 NULL 保存)。
14
+
15
+ - **`src/c3/db.py`: `read_tier_cost_summary` 追加**: tier_recent_outcomes × agent_cost_runs を session_id で JOIN し、complexity×tier 別の sessions / total_cost_usd / avg_cost_usd を返す。2 段 CTE で session コストの 1 session 内重複計上を防ぐ(mainline 除外・session 単位 SUM → DISTINCT JOIN)。テーブル不在・データ不在・session_id 全 NULL で `[]`。
16
+
17
+ - **`.claude/hooks/select_tier.py`: session_id 記録**: UserPromptSubmit payload から session_id を取得し tier_selection.json に追記。session_id が None のときはキーを省略(既存テスト互換)。
18
+
19
+ - **`.claude/skills/dev-workflow/scripts/record_tier_outcome.py`: session_id 受け渡し**: tier_selection.json から session_id を読み `record_tier_recent_outcome` に渡す。session_id キーが無い古い json でも動作(None として扱う)。
20
+
21
+ - **`c3 tier stats` に Tier 別平均コストセクション追加**: `_collect_snapshot()` に `read_tier_cost_summary()` の結果を `tier_cost` キーで追加。human 表示に「Tier 別平均コスト(粗い概算 / 精度向上は v2.23.0)」セクションを追加。tier_cost が空のときは「(cost 紐づけデータ未収集)」と表示。`--json` 出力の `tier_cost` キーにも自動反映。
22
+
23
+ ### 変更
24
+
25
+ - **`src/c3/__init__.py`**: `__version__` を `"2.21.0"` から `"2.22.0"` に更新。
26
+
27
+ - **注記更新**: `c3 tier stats` の「cost-aware routing は v2.22.0 予定」を「データ紐づけ蓄積。cost-aware routing 本体は v2.23.0 予定」へ更新。
28
+
29
+ ## [2.21.0] - 2026-05-25
30
+
31
+ **tier-routing コスト統合(データ収集基盤)**: Claude Code セッションログ(`~/.claude/projects/<slug>/<session>.jsonl` + subagent jsonl)を読み込み、モデル単価で USD 換算して c3.db に蓄積するデータ収集基盤を整備する。将来の cost-aware routing(v2.22.0)の土台。
32
+
33
+ **スコープ注記**: 本リリースはデータ収集基盤のみ。cost-aware routing と tier_bandit のコスト列は v2.22.0 予定。
34
+
35
+ ### 機能追加
36
+
37
+ - **`src/c3/pricing.py`(新規)**: Claude API モデルの USD/MTok 単価から token コストを計算する純関数モジュール。
38
+ `resolve_tier(model)`・`compute_cost_usd(...) -> tuple[float, bool]`・`known_models()` を提供。
39
+ Opus は世代で単価が 3 倍異なる(4.1/4=$15 系、4.5/4.6/4.7=$5 系)ため、具体パターン優先マッチ → tier 部分一致 fallback の 2 段構成を採用。
40
+ 単価は 2026-05-25 公式取得値(出典 URL を docstring に明記)。
41
+
42
+ - **`src/c3/migrations/002_agent_cost_runs.sql`(新規)**: `agent_cost_runs` テーブル・`usage_ingest_state` テーブル・インデックスを追加する migration。
43
+ PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」の集約設計。
44
+ 既存 event-based `agent_runs`(001)は一切変更しない。
45
+
46
+ - **`src/c3/usage_ingester.py`(新規)**: セッションログ取り込みモジュール。
47
+ 公開 API `ingest_session(*, session_id, project_dir, db_path=None) -> IngestResult`。
48
+ mainline / subagent jsonl を走査し、model 単位でトークンを合算して `insert_agent_cost_run` で upsert する。
49
+ session_id UUID validate・パストラバーサル防止・symlink スキップ・例外 type 名のみログ(SR-R-001 準拠)。
50
+
51
+ - **`.claude/hooks/session_stop.py` Phase 3 追加**: セッション終了時に `ingest_session` を呼ぶ Phase 3 を追加。
52
+ worktree session では起動しない。例外握りつぶしで exit 0 を維持する。
53
+
54
+ - **`c3 tier stats` の Agent 別コスト集計セクション追加**: `_collect_snapshot()` に `read_agent_cost_summary()` を追加し、human / JSON 両出力に `agent_cost` セクションを表示する。
55
+ mainline 行には「(マクロ集計・tier 学習対象外)」を明示。0 件のときは「(コストデータ未収集)」を表示。
56
+
57
+ ### 変更
58
+
59
+ - **`src/c3/db.py`**: 4 ヘルパー追加(`insert_agent_cost_run` / `read_agent_cost_summary` / `get_ingest_offset` / `set_ingest_offset`)。既存 tier ヘルパー規約(DB 不在で静かに False/0/[]・WAL・busy_timeout)に準拠。
60
+
61
+ - **`src/c3/__init__.py`**: `__version__` を `"2.20.0"` から `"2.21.0"` に更新。
62
+
3
63
  ## [2.20.0] - 2026-05-25
4
64
 
5
65
  **SQLite schema migration 枠組み導入**: `.claude/hooks/schema.sql` の「冪等 DDL 一発実行」から、`src/c3/migrations/` の「連番 NNN_xxx.sql migration runner」へ移行する基盤を整備する。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-conductor
3
- Version: 2.20.0
3
+ Version: 2.22.0
4
4
  Summary: Multi-agent orchestration framework for Claude Code with Codex/Cursor adapters (C3)
5
5
  Project-URL: Homepage, https://github.com/satoh-y-0323/claude-code-conductor
6
6
  Project-URL: Repository, https://github.com/satoh-y-0323/claude-code-conductor
@@ -1,3 +1,3 @@
1
1
  """Claude Code Conductor (C3) - multi-agent orchestration framework for Claude Code."""
2
2
 
3
- __version__ = "2.20.0"
3
+ __version__ = "2.22.0"
@@ -89,7 +89,7 @@ def handle_stats(args: argparse.Namespace) -> int:
89
89
 
90
90
 
91
91
  def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
92
- """DB から tier_bandit / tier_recent_outcomes を読み snapshot dict を返す。"""
92
+ """DB から tier_bandit / tier_recent_outcomes / agent_cost / tier_cost を読み snapshot dict を返す。"""
93
93
  bandit_rows: list[dict[str, Any]] = []
94
94
  total_trials = 0
95
95
 
@@ -114,6 +114,10 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
114
114
  db_path=db_path,
115
115
  )
116
116
 
117
+ agent_cost: list[dict[str, Any]] = c3_db.read_agent_cost_summary(db_path=db_path)
118
+
119
+ tier_cost: list[dict[str, Any]] = c3_db.read_tier_cost_summary(db_path=db_path)
120
+
117
121
  if total_trials < _LEARNING_THRESHOLD:
118
122
  mode = "uniform"
119
123
  else:
@@ -127,6 +131,8 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
127
131
  },
128
132
  "tier_bandit": bandit_rows,
129
133
  "recent_outcomes": recent_outcomes,
134
+ "agent_cost": agent_cost,
135
+ "tier_cost": tier_cost,
130
136
  }
131
137
 
132
138
 
@@ -166,3 +172,40 @@ def _render_human(snapshot: dict[str, Any]) -> None:
166
172
  print("== 学習データ記録チャネル ==")
167
173
  print("記録元: dev-workflow フェーズ E の最終承認時のみ(record_tier_outcome.py)")
168
174
  print("直接指示作業ではデータが溜まりません(設計通り)")
175
+ print()
176
+
177
+ print("== Agent 別コスト集計(agent_cost_runs) ==")
178
+ agent_cost = snapshot.get("agent_cost", [])
179
+ if not agent_cost:
180
+ print("(コストデータ未収集)")
181
+ else:
182
+ print(
183
+ f"{'agent_type':<16} {'runs':>5} {'total_usd':>10} "
184
+ f"{'in_tok':>9} {'out_tok':>9} {'cache_r':>9} {'cache_w':>9}"
185
+ )
186
+ for row in agent_cost:
187
+ note = " (マクロ集計・tier 学習対象外)" if row["agent_type"] == "mainline" else ""
188
+ print(
189
+ f"{row['agent_type']:<16} {row['runs']:>5} "
190
+ f"${row['total_cost_usd']:>9.4f} "
191
+ f"{row['input_tokens']:>9} {row['output_tokens']:>9} "
192
+ f"{row['cache_read_tokens']:>9} {row['cache_create_tokens']:>9}"
193
+ f"{note}"
194
+ )
195
+ print()
196
+
197
+ print("== Tier 別平均コスト(粗い概算 / 精度向上は v2.23.0) ==")
198
+ tier_cost = snapshot.get("tier_cost", [])
199
+ if not tier_cost:
200
+ print("(cost 紐づけデータ未収集)")
201
+ else:
202
+ print(f"{'complexity':<12} {'tier':<8} {'sessions':>8} {'avg_usd':>10} {'total_usd':>10}")
203
+ for row in tier_cost:
204
+ print(
205
+ f"{row['complexity']:<12} {row['tier']:<8} "
206
+ f"{row['sessions']:>8} "
207
+ f"${row['avg_cost_usd']:>9.4f} "
208
+ f"${row['total_cost_usd']:>9.4f}"
209
+ )
210
+ print()
211
+ print("(注: データ紐づけ蓄積。cost-aware routing 本体は v2.23.0 予定)")
@@ -358,12 +358,21 @@ def record_tier_recent_outcome(
358
358
  tier: str,
359
359
  success: bool,
360
360
  db_path: Path | None = None,
361
+ session_id: str | None = None,
361
362
  ) -> bool:
362
363
  """``tier_recent_outcomes`` に 1 件 INSERT する。
363
364
 
364
365
  Phase 2-B のエスカレーション判定用。tier_bandit の累積 α/β とは別に、
365
366
  直近 N 件の event を時系列で保持する。
366
367
 
368
+ Args:
369
+ complexity: 'simple' | 'medium' | 'complex'。
370
+ tier: 'haiku' | 'sonnet' | 'opus'。
371
+ success: True なら成功、False なら失敗。
372
+ db_path: c3.db のパス。省略時は :func:`locate_c3_db` で探索。
373
+ session_id: セッション UUID(v2.22.0+)。省略時は NULL で保存。
374
+ cost 紐づけに使用。None の場合は既存行との後方互換を維持する。
375
+
367
376
  Returns:
368
377
  INSERT 成功時 True、DB 不在 / エラー時 False。
369
378
  """
@@ -382,15 +391,15 @@ def record_tier_recent_outcome(
382
391
  _apply_busy_timeout(conn)
383
392
  conn.execute(
384
393
  "INSERT INTO tier_recent_outcomes "
385
- "(task_complexity, tier, success, ts) VALUES (?, ?, ?, ?)",
386
- (complexity, tier, 1 if success else 0, now_iso),
394
+ "(task_complexity, tier, success, ts, session_id) VALUES (?, ?, ?, ?, ?)",
395
+ (complexity, tier, 1 if success else 0, now_iso, session_id),
387
396
  )
388
397
  conn.commit()
389
398
  finally:
390
399
  conn.close()
391
400
  return True
392
401
  except Exception as exc: # noqa: BLE001
393
- logger.warning("failed to record tier_recent_outcome: %s", exc)
402
+ logger.warning("failed to record tier_recent_outcome: %s", type(exc).__name__)
394
403
  return False
395
404
 
396
405
 
@@ -493,3 +502,343 @@ def read_tier_failure_rate(
493
502
 
494
503
  failures = sum(1 for r in rows if r[0] == 0)
495
504
  return failures / sample_count, sample_count
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # usage-ingester: agent_cost_runs / usage_ingest_state ヘルパー(v2.21.0)
509
+ # ---------------------------------------------------------------------------
510
+
511
+
512
+ def insert_agent_cost_run(
513
+ *,
514
+ session_id: str,
515
+ agent_id: str,
516
+ agent_type: str,
517
+ description: str | None,
518
+ model: str,
519
+ attribution_skill: str | None,
520
+ input_tokens: int,
521
+ output_tokens: int,
522
+ cache_read_tokens: int,
523
+ cache_create_tokens: int,
524
+ total_cost_usd: float,
525
+ db_path: Path | None = None,
526
+ ) -> bool:
527
+ """agent_cost_runs に 1 行 upsert する。
528
+
529
+ PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」。
530
+ 同一 PK が既に存在する場合は全数値列・total_cost_usd・recorded_at を最新合算値に上書き。
531
+
532
+ Args:
533
+ session_id: セッション UUID。
534
+ agent_id: 'agent-<id>' または 'mainline'。
535
+ agent_type: meta.json の agentType / 'mainline'。
536
+ description: meta.json の description(任意)。
537
+ model: message.model 文字列。
538
+ attribution_skill: assistant レコードの attributionSkill(任意)。
539
+ input_tokens: 入力トークン数(jsonl 内合算値)。
540
+ output_tokens: 出力トークン数(jsonl 内合算値)。
541
+ cache_read_tokens: キャッシュ読み込みトークン数(jsonl 内合算値)。
542
+ cache_create_tokens: キャッシュ書き込みトークン数(jsonl 内合算値)。
543
+ total_cost_usd: compute_cost_usd が返した USD コスト。
544
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
545
+
546
+ Returns:
547
+ upsert 成功時 True、DB 不在 / sqlite3.Error 時は静かに False。
548
+ """
549
+ if db_path is None:
550
+ db_path = locate_c3_db()
551
+ if db_path is None:
552
+ return False
553
+
554
+ from datetime import timezone as _tz # noqa: PLC0415
555
+ now_iso = datetime.now(_tz.utc).isoformat(timespec="seconds")
556
+
557
+ try:
558
+ conn = sqlite3.connect(str(db_path))
559
+ try:
560
+ conn.execute("PRAGMA journal_mode=WAL")
561
+ _apply_busy_timeout(conn)
562
+ conn.execute(
563
+ "INSERT INTO agent_cost_runs "
564
+ "(session_id, agent_id, agent_type, description, model, "
565
+ " attribution_skill, input_tokens, output_tokens, "
566
+ " cache_read_tokens, cache_create_tokens, "
567
+ " total_cost_usd, recorded_at) "
568
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
569
+ "ON CONFLICT(session_id, agent_id, model) DO UPDATE SET "
570
+ " agent_type = excluded.agent_type, "
571
+ " description = excluded.description, "
572
+ " attribution_skill = excluded.attribution_skill, "
573
+ " input_tokens = excluded.input_tokens, "
574
+ " output_tokens = excluded.output_tokens, "
575
+ " cache_read_tokens = excluded.cache_read_tokens, "
576
+ " cache_create_tokens = excluded.cache_create_tokens, "
577
+ " total_cost_usd = excluded.total_cost_usd, "
578
+ " recorded_at = excluded.recorded_at",
579
+ (
580
+ session_id, agent_id, agent_type, description, model,
581
+ attribution_skill, input_tokens, output_tokens,
582
+ cache_read_tokens, cache_create_tokens,
583
+ total_cost_usd, now_iso,
584
+ ),
585
+ )
586
+ conn.commit()
587
+ finally:
588
+ conn.close()
589
+ return True
590
+ except Exception as exc: # noqa: BLE001
591
+ logger.warning("failed to insert_agent_cost_run: %s", type(exc).__name__)
592
+ return False
593
+
594
+
595
+ def read_agent_cost_summary(
596
+ *,
597
+ db_path: Path | None = None,
598
+ limit: int = 50,
599
+ ) -> list[dict]:
600
+ """agent_cost_runs を agent_type 別に集計した結果を返す。
601
+
602
+ SELECT agent_type, COUNT(*) runs, SUM(total_cost_usd), SUM(input_tokens),
603
+ SUM(output_tokens), SUM(cache_read_tokens), SUM(cache_create_tokens)
604
+ FROM agent_cost_runs GROUP BY agent_type ORDER BY total_cost_usd DESC LIMIT ?
605
+
606
+ Args:
607
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
608
+ limit: 返す最大件数(デフォルト 50)。
609
+
610
+ Returns:
611
+ 各行を dict にしたリスト。キー:
612
+ ``agent_type`` / ``runs`` / ``total_cost_usd`` /
613
+ ``input_tokens`` / ``output_tokens`` /
614
+ ``cache_read_tokens`` / ``cache_create_tokens``。
615
+ DB 不在 / テーブル不在 / エラー時は空リスト。
616
+ """
617
+ if db_path is None:
618
+ db_path = locate_c3_db()
619
+ if db_path is None:
620
+ return []
621
+
622
+ try:
623
+ conn = sqlite3.connect(str(db_path))
624
+ try:
625
+ # WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
626
+ # (既存 read_recent_outcomes 等の read ヘルパーと同方針 / CR-M-001)
627
+ _apply_busy_timeout(conn)
628
+ rows = conn.execute(
629
+ "SELECT agent_type, "
630
+ " COUNT(*) AS runs, "
631
+ " SUM(total_cost_usd) AS total_cost_usd, "
632
+ " SUM(input_tokens) AS input_tokens, "
633
+ " SUM(output_tokens) AS output_tokens, "
634
+ " SUM(cache_read_tokens) AS cache_read_tokens, "
635
+ " SUM(cache_create_tokens) AS cache_create_tokens "
636
+ "FROM agent_cost_runs "
637
+ "GROUP BY agent_type "
638
+ "ORDER BY total_cost_usd DESC "
639
+ "LIMIT ?",
640
+ (limit,),
641
+ ).fetchall()
642
+ finally:
643
+ conn.close()
644
+ except sqlite3.OperationalError as exc:
645
+ # テーブル不在(no such table)は [] を返す(DB 未初期化でも止めない)
646
+ logger.debug(
647
+ "read_agent_cost_summary: table not found or inaccessible: %s",
648
+ type(exc).__name__,
649
+ )
650
+ return []
651
+ except Exception as exc: # noqa: BLE001
652
+ logger.warning("read_agent_cost_summary: unexpected error: %s", type(exc).__name__)
653
+ return []
654
+
655
+ return [
656
+ {
657
+ "agent_type": row[0],
658
+ "runs": row[1],
659
+ "total_cost_usd": row[2],
660
+ "input_tokens": row[3],
661
+ "output_tokens": row[4],
662
+ "cache_read_tokens": row[5],
663
+ "cache_create_tokens": row[6],
664
+ }
665
+ for row in rows
666
+ ]
667
+
668
+
669
+ def read_tier_cost_summary(
670
+ *,
671
+ db_path: Path | None = None,
672
+ ) -> list[dict]:
673
+ """tier_recent_outcomes × agent_cost_runs を session_id で JOIN し、
674
+ complexity×tier 別コストを返す(粗い概算 / 精度向上は v2.23.0)。
675
+
676
+ 2 段 CTE で重複計上を防ぐ:
677
+ - ``session_cost`` CTE: agent_cost_runs を mainline 除外しつつ
678
+ session_id 単位で SUM(1 session = 1 行)→ cost の重複計上を構造的に防ぐ
679
+ - ``outcome_sessions`` CTE: tier_recent_outcomes を
680
+ (session_id, task_complexity, tier) で DISTINCT 化。
681
+ session_id が NULL(v2.22.0 移行前行)は除外
682
+ - JOIN して complexity×tier 別に sessions / total_cost_usd / avg_cost_usd を算出
683
+
684
+ 既知の不正確性(v2.22.0 許容・精度向上は v2.23.0):
685
+ - session が複数の (complexity, tier) outcome を持つ場合、同一 session の cost が
686
+ 複数の (complexity,tier) 行に重複加算されうる(1 session 内での cost 重複は防ぐが、
687
+ 複数 outcome を持つ session の cross-outcome 重複は未解決)
688
+ - cost は session 全体の non-mainline 合計であり、特定 tier の model 行に限定しない
689
+ - agent_id 単位の紐づけはしていない
690
+
691
+ Args:
692
+ db_path: c3.db のパス。省略時は :func:`locate_c3_db` で探索。
693
+
694
+ Returns:
695
+ 各行を dict にしたリスト。キー:
696
+ ``complexity``(str) / ``tier``(str) / ``sessions``(int) /
697
+ ``total_cost_usd``(float) / ``avg_cost_usd``(float)。
698
+ ORDER BY total_cost_usd DESC。
699
+ テーブル不在 / データ不在 / session_id 全 NULL / JOIN 0 行 /
700
+ DB 不在 / エラー時は空リストを返す。
701
+ """
702
+ if db_path is None:
703
+ db_path = locate_c3_db()
704
+ if db_path is None:
705
+ return []
706
+
707
+ try:
708
+ conn = sqlite3.connect(str(db_path))
709
+ try:
710
+ # WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
711
+ # (read_agent_cost_summary 等の read ヘルパーと同方針 / CR-M-001)
712
+ _apply_busy_timeout(conn)
713
+ rows = conn.execute(
714
+ "WITH session_cost AS ("
715
+ " SELECT session_id,"
716
+ " SUM(total_cost_usd) AS session_cost_usd"
717
+ " FROM agent_cost_runs"
718
+ " WHERE agent_type <> 'mainline'"
719
+ " GROUP BY session_id"
720
+ "),"
721
+ "outcome_sessions AS ("
722
+ " SELECT DISTINCT session_id, task_complexity, tier"
723
+ " FROM tier_recent_outcomes"
724
+ " WHERE session_id IS NOT NULL"
725
+ ") "
726
+ "SELECT o.task_complexity AS complexity,"
727
+ " o.tier AS tier,"
728
+ " COUNT(DISTINCT o.session_id) AS sessions,"
729
+ " SUM(sc.session_cost_usd) AS total_cost_usd,"
730
+ " SUM(sc.session_cost_usd) / COUNT(DISTINCT o.session_id)"
731
+ " AS avg_cost_usd "
732
+ "FROM outcome_sessions o "
733
+ "JOIN session_cost sc ON sc.session_id = o.session_id "
734
+ "GROUP BY o.task_complexity, o.tier "
735
+ "ORDER BY total_cost_usd DESC"
736
+ ).fetchall()
737
+ finally:
738
+ conn.close()
739
+ except sqlite3.OperationalError as exc:
740
+ # テーブル不在(no such table)は [] を返す(DB 未初期化でも止めない)
741
+ logger.debug(
742
+ "read_tier_cost_summary: table not found or inaccessible: %s",
743
+ type(exc).__name__,
744
+ )
745
+ return []
746
+ except Exception as exc: # noqa: BLE001
747
+ logger.warning("read_tier_cost_summary: unexpected error: %s", type(exc).__name__)
748
+ return []
749
+
750
+ return [
751
+ {
752
+ "complexity": row[0],
753
+ "tier": row[1],
754
+ "sessions": int(row[2]),
755
+ "total_cost_usd": float(row[3]) if row[3] is not None else 0.0,
756
+ "avg_cost_usd": float(row[4]) if row[4] is not None else 0.0,
757
+ }
758
+ for row in rows
759
+ ]
760
+
761
+
762
+ def get_ingest_offset(
763
+ file_key: str,
764
+ *,
765
+ db_path: Path | None = None,
766
+ ) -> int:
767
+ """usage_ingest_state から処理済み行数(offset)を返す。
768
+
769
+ Args:
770
+ file_key: '<session>:mainline' / '<session>:agent-<id>'。
771
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
772
+
773
+ Returns:
774
+ 処理済み行数。行・テーブル不在・DB 不在は 0 を返す。
775
+ """
776
+ if db_path is None:
777
+ db_path = locate_c3_db()
778
+ if db_path is None:
779
+ return 0
780
+
781
+ try:
782
+ conn = sqlite3.connect(str(db_path))
783
+ try:
784
+ # WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
785
+ # (既存 read_recent_outcomes 等の read ヘルパーと同方針 / CR-M-001)
786
+ _apply_busy_timeout(conn)
787
+ row = conn.execute(
788
+ "SELECT last_offset FROM usage_ingest_state WHERE file_key = ?",
789
+ (file_key,),
790
+ ).fetchone()
791
+ finally:
792
+ conn.close()
793
+ except Exception as exc: # noqa: BLE001
794
+ logger.debug("get_ingest_offset: %s", type(exc).__name__)
795
+ return 0
796
+
797
+ return row[0] if row is not None else 0
798
+
799
+
800
+ def set_ingest_offset(
801
+ file_key: str,
802
+ offset: int,
803
+ *,
804
+ db_path: Path | None = None,
805
+ ) -> bool:
806
+ """usage_ingest_state に offset を upsert する。
807
+
808
+ Args:
809
+ file_key: '<session>:mainline' / '<session>:agent-<id>'。
810
+ offset: 処理済み行数(= 次回開始行)。
811
+ db_path: c3.db のパス。省略時は locate_c3_db() で探索。
812
+
813
+ Returns:
814
+ upsert 成功時 True、DB 不在 / エラー時は静かに False。
815
+ """
816
+ if db_path is None:
817
+ db_path = locate_c3_db()
818
+ if db_path is None:
819
+ return False
820
+
821
+ from datetime import timezone as _tz # noqa: PLC0415
822
+ now_iso = datetime.now(_tz.utc).isoformat(timespec="seconds")
823
+
824
+ try:
825
+ conn = sqlite3.connect(str(db_path))
826
+ try:
827
+ conn.execute("PRAGMA journal_mode=WAL")
828
+ _apply_busy_timeout(conn)
829
+ conn.execute(
830
+ "INSERT INTO usage_ingest_state "
831
+ "(file_key, last_offset, last_processed_at) "
832
+ "VALUES (?, ?, ?) "
833
+ "ON CONFLICT(file_key) DO UPDATE SET "
834
+ " last_offset = excluded.last_offset, "
835
+ " last_processed_at = excluded.last_processed_at",
836
+ (file_key, offset, now_iso),
837
+ )
838
+ conn.commit()
839
+ finally:
840
+ conn.close()
841
+ return True
842
+ except Exception as exc: # noqa: BLE001
843
+ logger.warning("failed to set_ingest_offset: %s", type(exc).__name__)
844
+ return False
@@ -0,0 +1,41 @@
1
+ -- C3 SQLite migration 002: agent_cost_runs / usage_ingest_state テーブル追加
2
+ --
3
+ -- セッションログ(~/.claude/projects/<slug>/<session>.jsonl + subagents/)から
4
+ -- モデル別トークン消費を USD 換算して蓄積するコスト収集基盤テーブルを定義する。
5
+ --
6
+ -- PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」。
7
+ -- jsonl 内の複数 requestId は ingester(usage_ingester.py)が合算 upsert する。
8
+ --
9
+ -- usage_ingest_state は jsonl ごとの処理済み行数 offset を管理する。
10
+ -- file_key = '<session>:mainline' / '<session>:agent-<id>'
11
+
12
+ BEGIN;
13
+
14
+ CREATE TABLE IF NOT EXISTS agent_cost_runs (
15
+ session_id TEXT NOT NULL,
16
+ agent_id TEXT NOT NULL, -- 'agent-<id>' / mainline は 'mainline'
17
+ agent_type TEXT NOT NULL, -- meta.json agentType / 'mainline'
18
+ description TEXT,
19
+ model TEXT NOT NULL, -- message.model
20
+ attribution_skill TEXT,
21
+ input_tokens INTEGER NOT NULL DEFAULT 0,
22
+ output_tokens INTEGER NOT NULL DEFAULT 0,
23
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
24
+ cache_create_tokens INTEGER NOT NULL DEFAULT 0,
25
+ total_cost_usd REAL NOT NULL,
26
+ recorded_at TEXT NOT NULL, -- ISO8601 UTC
27
+ PRIMARY KEY (session_id, agent_id, model)
28
+ );
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_agent_cost_runs_agent_type
31
+ ON agent_cost_runs(agent_type, recorded_at);
32
+
33
+ CREATE TABLE IF NOT EXISTS usage_ingest_state (
34
+ file_key TEXT PRIMARY KEY, -- '<session>:mainline' / '<session>:agent-<id>'
35
+ last_offset INTEGER NOT NULL, -- 処理済み行数
36
+ last_processed_at TEXT NOT NULL -- ISO8601 UTC
37
+ );
38
+
39
+ INSERT OR IGNORE INTO schema_migrations (version) VALUES ('002');
40
+
41
+ COMMIT;