claude-code-conductor 2.26.0__tar.gz → 2.28.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 (193) hide show
  1. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/select_tier.py +17 -12
  2. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/CHANGELOG.md +41 -0
  3. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/PKG-INFO +2 -2
  4. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/README.md +1 -1
  5. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/__init__.py +1 -1
  6. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_recall.py +142 -19
  7. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_tier.py +20 -0
  8. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/db.py +121 -0
  9. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/recall_index.py +28 -3
  10. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_select_tier.py +159 -6
  11. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_session_start.py +16 -4
  12. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_init.py +10 -0
  13. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_recall.py +379 -0
  14. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_tier.py +106 -0
  15. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_db.py +210 -0
  16. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_recall_index.py +119 -0
  17. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/CLAUDE.md +0 -0
  18. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/architect.md +0 -0
  19. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/code-reviewer.md +0 -0
  20. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/developer.md +0 -0
  21. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/doc-writer.md +0 -0
  22. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/interviewer.md +0 -0
  23. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/planner.md +0 -0
  24. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/project-setup.md +0 -0
  25. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/security-reviewer.md +0 -0
  26. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/systematic-debugger.md +0 -0
  27. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/tester.md +0 -0
  28. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/wt_developer.md +0 -0
  29. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/wt_systematic-debugger.md +0 -0
  30. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/agents/wt_tester.md +0 -0
  31. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/breaking-changes.txt +0 -0
  32. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/deletions.txt +0 -0
  33. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/docs/config-policy.md +0 -0
  34. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/docs/parallel-agents-setup.md +0 -0
  35. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/docs/platform-adapters.md +0 -0
  36. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/docs/settings.json.md +0 -0
  37. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/_hook_utils.py +0 -0
  38. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/check_agent_invocation.py +0 -0
  39. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/consolidate_memory.py +0 -0
  40. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/permission_handler.py +0 -0
  41. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/permission_handler_toast.py +0 -0
  42. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/planner_check.py +0 -0
  43. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/post_tool.py +0 -0
  44. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/pre_compact.py +0 -0
  45. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/pre_tool.py +0 -0
  46. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/recall_inject.py +0 -0
  47. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/restore_session.py +0 -0
  48. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/session_start.py +0 -0
  49. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/session_stop.py +0 -0
  50. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/session_utils.py +0 -0
  51. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/statusline.py +0 -0
  52. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/stop.py +0 -0
  53. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/hooks/worktree_guard.py +0 -0
  54. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/memory/.gitkeep +0 -0
  55. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/permission_rules.json +0 -0
  56. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/rules/promoted/index.md +0 -0
  57. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/settings.json +0 -0
  58. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/codex-review/SKILL.md +0 -0
  59. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/dev-workflow/SKILL.md +0 -0
  60. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
  61. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
  62. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
  63. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +0 -0
  64. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/dev-workflow/scripts/record_tier_outcome.py +0 -0
  65. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
  66. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/develop/SKILL.md +0 -0
  67. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/doc/SKILL.md +0 -0
  68. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/extract-lib/SKILL.md +0 -0
  69. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/init-session/SKILL.md +0 -0
  70. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/mcp-config/SKILL.md +0 -0
  71. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/parallel-agents/SKILL.md +0 -0
  72. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/pattern-status/SKILL.md +0 -0
  73. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
  74. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/recall/SKILL.md +0 -0
  75. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
  76. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
  77. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/review-phase/SKILL.md +0 -0
  78. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/setup/SKILL.md +0 -0
  79. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/setup/reference.md +0 -0
  80. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
  81. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
  82. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/skills/start/SKILL.md +0 -0
  83. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.claude/state/.gitkeep +0 -0
  84. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/.gitignore +0 -0
  85. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/LICENSE +0 -0
  86. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/LICENSES/chroma-hnswlib-LICENSE +0 -0
  87. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/LICENSES/chroma-hnswlib-NOTICE +0 -0
  88. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/LICENSES/fastembed-LICENSE +0 -0
  89. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/LICENSES/fastembed-NOTICE +0 -0
  90. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/LICENSES/onnxruntime-LICENSE +0 -0
  91. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
  92. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/hatch_build.py +0 -0
  93. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/pyproject.toml +0 -0
  94. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/__main__.py +0 -0
  95. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/_excludes.py +0 -0
  96. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/_terminal.py +0 -0
  97. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/adapters.py +0 -0
  98. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli.py +0 -0
  99. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_ask.py +0 -0
  100. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_doctor.py +0 -0
  101. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_init.py +0 -0
  102. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_list.py +0 -0
  103. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_plan.py +0 -0
  104. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/cli_update.py +0 -0
  105. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/embedding.py +0 -0
  106. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/mcp_server.py +0 -0
  107. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/migrate.py +0 -0
  108. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/migrations/001_initial.sql +0 -0
  109. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/migrations/002_agent_cost_runs.sql +0 -0
  110. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/migrations/003_tier_cost.sql +0 -0
  111. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/migrations/README.md +0 -0
  112. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/migrations/__init__.py +0 -0
  113. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/paths.py +0 -0
  114. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/plan_validator.py +0 -0
  115. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/platforms.py +0 -0
  116. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/pricing.py +0 -0
  117. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/question.py +0 -0
  118. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/recall_chunker.py +0 -0
  119. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/src/c3/usage_ingester.py +0 -0
  120. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/__init__.py +0 -0
  121. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/conftest.py +0 -0
  122. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/fixtures/usage/README.md +0 -0
  123. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/fixtures/usage/mainline.jsonl +0 -0
  124. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +0 -0
  125. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +0 -0
  126. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/__init__.py +0 -0
  127. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_check_agent_invocation.py +0 -0
  128. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_consolidate_memory.py +0 -0
  129. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_hook_utils.py +0 -0
  130. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_permission_handler.py +0 -0
  131. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_permission_handler_toast.py +0 -0
  132. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
  133. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_planner_check.py +0 -0
  134. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_planner_check_dev.py +0 -0
  135. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_post_tool.py +0 -0
  136. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_pre_tool.py +0 -0
  137. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_recall_inject.py +0 -0
  138. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_record_review_decision.py +0 -0
  139. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_record_tier_outcome.py +0 -0
  140. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_restore_session.py +0 -0
  141. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_review_hint_inject.py +0 -0
  142. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_select_tier_escalation.py +0 -0
  143. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_session_stop.py +0 -0
  144. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_session_utils.py +0 -0
  145. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
  146. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_similarity_boost.py +0 -0
  147. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_statusline.py +0 -0
  148. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_statusline_template_sync.py +0 -0
  149. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_sync_check.py +0 -0
  150. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/hooks/test_template_guard.py +0 -0
  151. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/__init__.py +0 -0
  152. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/_skill_helpers.py +0 -0
  153. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
  154. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_init_session_no_task_type.py +0 -0
  155. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_planner_lightweight.py +0 -0
  156. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_recall_skill.py +0 -0
  157. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
  158. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_setup_templates.py +0 -0
  159. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
  160. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_start_skill_new_flow.py +0 -0
  161. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
  162. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_adapters.py +0 -0
  163. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_ask.py +0 -0
  164. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_entry.py +0 -0
  165. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_list.py +0 -0
  166. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_plan.py +0 -0
  167. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_update_breaking_changes.py +0 -0
  168. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_cli_update_deletions.py +0 -0
  169. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_docstring_consistency.py +0 -0
  170. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_embedding.py +0 -0
  171. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_excludes.py +0 -0
  172. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_extract_breaking_changes.py +0 -0
  173. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_mcp_server_elicit.py +0 -0
  174. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_migrate.py +0 -0
  175. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_paths.py +0 -0
  176. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_plan_validator.py +0 -0
  177. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_pre_compact.py +0 -0
  178. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_pre_tool_hook.py +0 -0
  179. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_precompact_additional.py +0 -0
  180. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_precompact_toctou_fixes.py +0 -0
  181. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_pricing.py +0 -0
  182. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_recall_chunker.py +0 -0
  183. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_references_migration.py +0 -0
  184. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_session_utils_additional.py +0 -0
  185. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_skill_no_builtin_conflict.py +0 -0
  186. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_statusline.py +0 -0
  187. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_stop_additional.py +0 -0
  188. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_stop_hook.py +0 -0
  189. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_stop_precompact_fixes.py +0 -0
  190. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_sync_template_stop.py +0 -0
  191. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_template_pre_tool_hook.py +0 -0
  192. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_usage_ingester.py +0 -0
  193. {claude_code_conductor-2.26.0 → claude_code_conductor-2.28.0}/tests/test_worktree_guard.py +0 -0
@@ -51,11 +51,15 @@ try:
51
51
  EPSILON: float = _c3_db_const.EPSILON_TIEBREAK
52
52
  ESCALATION_THRESHOLD: float = _c3_db_const.ESCALATION_THRESHOLD_DEFAULT
53
53
  COST_LAMBDA_DEFAULT: float | None = _c3_db_const.COST_LAMBDA_DEFAULT
54
+ COST_LAMBDA_MIN: float = _c3_db_const.COST_LAMBDA_MIN
55
+ COST_LAMBDA_MAX: float = _c3_db_const.COST_LAMBDA_MAX
54
56
  except ImportError:
55
57
  LEARNING_THRESHOLD = 30
56
58
  EPSILON = 0.05
57
59
  ESCALATION_THRESHOLD = 0.5
58
60
  COST_LAMBDA_DEFAULT = None
61
+ COST_LAMBDA_MIN = 0.0
62
+ COST_LAMBDA_MAX = 5.0
59
63
 
60
64
  # 複雑度推定のキーワード
61
65
  SIMPLE_KEYWORDS = frozenset({
@@ -174,14 +178,14 @@ def _cost_tiebreak(
174
178
  epsilon: 拮抗判定の閾値(デフォルト EPSILON=0.05)。経路 0/1 で使用。
175
179
  経路 2 では contenders 算出にのみ使用(選択自体は全 tier score 比較)。
176
180
  lam: cost weighting 係数(λ)。None=センチネル(経路 1・後方互換)、
177
- 0.0=cost 無視明示(経路 0)、0 < lam <= 1=全 tier weighting(経路 2)。
181
+ 0.0=cost 無視明示(経路 0)、0 < lam <= COST_LAMBDA_MAX=全 tier weighting(経路 2)。
178
182
  デフォルト None で既存 2 引数呼び出しの挙動・シグネチャを完全不変にする。
179
183
 
180
184
  Returns:
181
185
  (chosen, did_tiebreak, contenders) のタプル。
182
186
  - chosen: 選択された tier 名。
183
187
  - did_tiebreak: cost が選択に影響した場合 True。
184
- 経路 1: contenders 内 min-max で安い方を選んだ場合 True
188
+ 経路 1: contenders 内 min-max で安い方を選んだ場合 True(全 tier コスト同値時は False)。
185
189
  経路 2: 全 tier weighting で argmax(sample) と異なる選択になった場合 True。
186
190
  - contenders: ε 拮抗判定に入った tier のタプル(observability 用)。
187
191
  """
@@ -202,11 +206,12 @@ def _cost_tiebreak(
202
206
  # 拮抗群内で min-max 正規化コストを計算し最安 tier を選ぶ
203
207
  costs = {t: cost_map[t] for t in contenders}
204
208
  lo, hi = min(costs.values()), max(costs.values())
205
- norm = {t: ((costs[t] - lo) / (hi - lo) if hi > lo else 0.0) for t in contenders}
206
- # 同値安定 tie-break: norm 同値時はサンプル大(=従来選好)を優先 決定論
207
- # 注: tier コスト同値(hi == lo → norm 全 0.0)でも did_tiebreak=True を返す。
208
- # これは v2.25.0 のバイト互換維持のため意図的(フラグ意味の精緻化=同値時 False
209
- # 後方互換を崩すため v2.27.0+ で扱う)。[CR-Q-001]
209
+ if hi == lo:
210
+ # [CR-Q-001] tier コスト同値: cost は選択に無関与。
211
+ # argmax(sample) を返し did_tiebreak=False(observability 精緻化・v2.27.0 で精緻化済み)。
212
+ chosen = max(samples, key=lambda t: samples[t])
213
+ return chosen, False, tuple(contenders)
214
+ norm = {t: (costs[t] - lo) / (hi - lo) for t in contenders}
210
215
  chosen = min(contenders, key=lambda t: (norm[t], -samples[t]))
211
216
  return chosen, True, tuple(contenders)
212
217
 
@@ -359,7 +364,7 @@ def select_tier_detailed(
359
364
  uniform 分岐では cost_map の有無に関わらず完全無視する(探索保護)。
360
365
  epsilon: 拮抗判定閾値。None なら module 定数 EPSILON を使う(C3_TIER_EPSILON で上書き可)。
361
366
  lam: cost weighting 係数(λ)。None=センチネル(v2.25.0 ε-gated 後方互換)、
362
- 0.0=cost 無視明示、0<lam<=1=全 tier weighting 発動(C3_TIER_COST_LAMBDA で上書き可)。
367
+ 0.0=cost 無視明示、0<lam<=COST_LAMBDA_MAX=全 tier weighting 発動(C3_TIER_COST_LAMBDA で上書き可)。
363
368
  None(デフォルト)では env 未設定時と完全一致する(後方互換の核心)。
364
369
  uniform 分岐では lam の値に関わらず完全無視する(探索保護・不可侵)。
365
370
 
@@ -685,9 +690,9 @@ def _resolve_escalation_threshold() -> float:
685
690
  def _resolve_cost_lambda() -> float | None:
686
691
  """``C3_TIER_COST_LAMBDA`` を安全に解決する。
687
692
 
688
- 不正値(非数値 / 0 未満 / 1 超 / NaN)は受け付けず、stderr 警告 + デフォルト(COST_LAMBDA_DEFAULT)に戻す。
693
+ 不正値(非数値 / 0 未満 / COST_LAMBDA_MAX 超 / NaN)は受け付けず、stderr 警告 + デフォルト(COST_LAMBDA_DEFAULT)に戻す。
689
694
  未設定 / 空文字は無警告でデフォルト(None)を返す。
690
- 妥当域: 0 <= x <= 1(x == 0 は許容=cost 無視の明示オプト・_resolve_epsilon と異なり下限を含む)。区間表記: [0, 1](x=0 許容のため閉区間)。
695
+ 妥当域: 0 <= x <= COST_LAMBDA_MAX(x == 0 は許容=cost 無視の明示オプト・_resolve_epsilon と異なり下限を含む)。区間表記: [0, COST_LAMBDA_MAX](x=0 許容のため閉区間)。
691
696
  戻り値が None の場合は v2.25.0 互換の ε tie-break 経路を維持する(センチネル)。
692
697
  """
693
698
  raw = os.environ.get("C3_TIER_COST_LAMBDA")
@@ -709,9 +714,9 @@ def _resolve_cost_lambda() -> float | None:
709
714
  file=sys.stderr,
710
715
  )
711
716
  return COST_LAMBDA_DEFAULT
712
- if x < 0 or x > 1:
717
+ if x < COST_LAMBDA_MIN or x > COST_LAMBDA_MAX:
713
718
  print(
714
- f"[select_tier:cost_lambda] C3_TIER_COST_LAMBDA={x!r} out of range [0, 1], "
719
+ f"[select_tier:cost_lambda] C3_TIER_COST_LAMBDA={x!r} out of range [0, {COST_LAMBDA_MAX}], "
715
720
  f"using default {COST_LAMBDA_DEFAULT}",
716
721
  file=sys.stderr,
717
722
  )
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.28.0] - 2026-05-27
4
+
5
+ **recall 増分 rebuild**: `c3 recall rebuild` を全再構築から増分に最適化。未変更チャンクは既存インデックスのベクトルを再利用し、変更/新規チャンクのみ再埋め込みする。律速の埋め込み(fastembed 推論)を削減して rebuild を高速化する。検索結果・インデックス形式は全再構築と一致。**破壊的変更なし**。
6
+
7
+ ### 機能追加
8
+
9
+ - **`src/c3/cli_recall.py`: `c3 recall rebuild` の増分化**: `(source_type, path, chunk_id)` と `source_hash`(v2 で既に保存済み)が一致する未変更チャンクは旧ベクトルを再利用し、変更/新規チャンクのみ `embed_passages` に渡す。出力は `embedded M / reused K chunks` 形式。`--force` 指定時は従来どおり全再構築。
10
+ - **`src/c3/recall_index.py`: `RecallIndex.get_vector(chunk_id)` / 公開 `content_hash(text)` を追加**: `get_vector` は hnswlib 格納ベクトルを取得(増分時の再利用に使用)。`content_hash` は source_hash 計算を一元化した公開ヘルパー(`build` と `cli_recall` が共用)。
11
+
12
+ ### 変更
13
+
14
+ - **増分不可時の安全フォールバック**: 既存インデックス不在・`--force`・`load()` 失敗(model/dim 不一致・破損)の場合は全再構築にフォールバックし、stderr に理由(例外型名のみ)を 1 行出力する。
15
+
16
+ ### 後方互換
17
+
18
+ - 検索結果・インデックス形式は全再構築と完全一致(増分はベクトル再利用のみで意味論を変えない)。
19
+ - `--force` で従来の全再構築を維持。
20
+ - migration 不要。**破壊的変更なし**。
21
+
22
+ ## [2.27.0] - 2026-05-26
23
+
24
+ **tier-routing λ 機能拡張(CR-Q-001 精緻化・λ 上限 5.0・cli_tier routing パラメータ表示)**: v2.26.0 で繰り越した 3 項目を解消。λ の上限を 1.0 から 5.0 に拡張、cost-aware tie-break の observability フラグを精緻化、`c3 tier stats` に現在の routing パラメータ(λ/ε/escalation)を表示。環境変数未設定時の routing 出力は v2.26.0 と一致。**破壊的変更なし**。
25
+
26
+ ### 機能追加
27
+
28
+ - **`src/c3/db.py`: `COST_LAMBDA_MIN`(0.0)/ `COST_LAMBDA_MAX`(5.0)定数を追加(SSOT)**: cost-weighted Thompson の λ 妥当域の上限を 1.0 から 5.0 に拡張。これにより最高コスト tier の sample をより強く減点でき、cost を成功率より優先させる調整が可能になる。既存の `[0, 1]` の λ 値は引き続き有効(後方互換)。
29
+ - **`src/c3/db.py`: 公開 `resolve_cost_lambda` / `resolve_epsilon` / `resolve_escalation_threshold` を追加**: 環境変数(`C3_TIER_COST_LAMBDA` / `C3_TIER_EPSILON` / `C3_ESCALATION_THRESHOLD`)の解決ロジックを db.py に SSOT として新設。`cli_tier.py` が現在有効な routing パラメータを表示するために参照する。挙動は hook(`select_tier.py`)の既存 `_resolve_*` と一致し、parity テストで戻り値の一致を担保。
30
+ - **`src/c3/cli_tier.py`: `c3 tier stats` に「routing パラメータ」セクションを追加**: 現在有効な λ(`C3_TIER_COST_LAMBDA`)・ε(`C3_TIER_EPSILON`)・escalation threshold(`C3_ESCALATION_THRESHOLD`)を表示。λ は未設定(v2.25.0 互換)/ 0.0(cost 無視)/ 0 < x ≤ 5(全 tier weighting)で文言を分岐。`--json` 出力にも `routing_params` キーを追加。
31
+
32
+ ### 変更
33
+
34
+ - **`.claude/hooks/select_tier.py`: `_resolve_cost_lambda` の上限を `COST_LAMBDA_MAX`(5.0)参照に変更**: 従来ハードコードの上限 `1` を db.py の SSOT 定数参照に変更(import 失敗時フォールバック 5.0)。下限も `COST_LAMBDA_MIN`(0.0)参照に統一。
35
+ - **`.claude/hooks/select_tier.py`: CR-Q-001 — `_cost_tiebreak` 経路1 の observability フラグ精緻化**: v2.25.0 互換の ε tie-break 経路で、拮抗群の全 tier コストが同値(`hi == lo`)の場合に `did_tiebreak=False` を返すよう変更。**選ばれる tier は不変**(`argmax(sample)`)で、変わるのは observability のみ。
36
+
37
+ ### 後方互換
38
+
39
+ - 環境変数未設定時の routing 挙動・選ばれる tier は v2.26.0 と完全一致。
40
+ - λ の既存値(`[0, 1]`)は引き続き有効。上限拡張は許容域の拡大のみ。
41
+ - **observability 出力の差分(CR-Q-001)**: cost-aware tie-break で**全 tier コストが同値**の特定ケースに限り、`tier_selection.json` の `cost_tiebreak: true` キーが**省略**されるようになる(従来は `true` を出力)。routing 決定(選ばれる tier)には影響しない。
42
+ - migration 不要。**破壊的変更なし**。
43
+
3
44
  ## [2.26.0] - 2026-05-26
4
45
 
5
46
  **cost-weighted Thompson 本格統合(全 tier)・ESCALATION_THRESHOLD 調整可能化**: Thompson Sampling のサンプル値を全 tier でコスト重み付けして routing する機能を導入。failure-rate escalation 閾値を環境変数で調整可能にする。環境変数 3 種すべて未設定で v2.25.0 と完全一致。**破壊的変更なし**。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-conductor
3
- Version: 2.26.0
3
+ Version: 2.28.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
@@ -201,7 +201,7 @@ C3 のスラッシュコマンドはすべてスキル(`skills/{name}/SKILL.md
201
201
  | `c3 recall search "<query>"` または `c3 recall "<query>"` | `.claude/memory/sessions/` 等から類似チャンクを意味検索 |
202
202
  | `c3 recall rebuild [--force]` | HNSW インデックスを再構築(初回は fastembed が ~220MB のモデルを取得) |
203
203
  | `c3 recall stats` | チャンク数・モデル名・最終 rebuild 日時を表示 |
204
- | `c3 tier stats` | tier-routing(複雑度に応じた Tier 自動ルーティング)の学習データ・Tier 別コストを表形式で表示(`--json` で機械可読出力・`--recent N` で直近 outcome 件数指定)。ルーティング挙動は環境変数 `C3_TIER_COST_LAMBDA`(cost-weighted の重み・v2.26.0〜)/ `C3_TIER_EPSILON` / `C3_ESCALATION_THRESHOLD` で調整可([CLI リファレンス](https://satoh-y-0323.github.io/claude-code-conductor/cli-reference/)参照) |
204
+ | `c3 tier stats` | tier-routing(複雑度に応じた Tier 自動ルーティング)の学習データ・Tier 別コスト・現在の routing パラメータ(λ/ε/escalation・v2.27.0〜)を表形式で表示(`--json` で機械可読出力・`--recent N` で直近 outcome 件数指定)。ルーティング挙動は環境変数 `C3_TIER_COST_LAMBDA`(cost-weighted の重み・`0 ≤ λ ≤ 5`・v2.26.0〜、上限拡張 v2.27.0)/ `C3_TIER_EPSILON` / `C3_ESCALATION_THRESHOLD` で調整可([CLI リファレンス](https://satoh-y-0323.github.io/claude-code-conductor/cli-reference/)参照) |
205
205
 
206
206
  ### 基本的な使い方
207
207
 
@@ -154,7 +154,7 @@ C3 のスラッシュコマンドはすべてスキル(`skills/{name}/SKILL.md
154
154
  | `c3 recall search "<query>"` または `c3 recall "<query>"` | `.claude/memory/sessions/` 等から類似チャンクを意味検索 |
155
155
  | `c3 recall rebuild [--force]` | HNSW インデックスを再構築(初回は fastembed が ~220MB のモデルを取得) |
156
156
  | `c3 recall stats` | チャンク数・モデル名・最終 rebuild 日時を表示 |
157
- | `c3 tier stats` | tier-routing(複雑度に応じた Tier 自動ルーティング)の学習データ・Tier 別コストを表形式で表示(`--json` で機械可読出力・`--recent N` で直近 outcome 件数指定)。ルーティング挙動は環境変数 `C3_TIER_COST_LAMBDA`(cost-weighted の重み・v2.26.0〜)/ `C3_TIER_EPSILON` / `C3_ESCALATION_THRESHOLD` で調整可([CLI リファレンス](https://satoh-y-0323.github.io/claude-code-conductor/cli-reference/)参照) |
157
+ | `c3 tier stats` | tier-routing(複雑度に応じた Tier 自動ルーティング)の学習データ・Tier 別コスト・現在の routing パラメータ(λ/ε/escalation・v2.27.0〜)を表形式で表示(`--json` で機械可読出力・`--recent N` で直近 outcome 件数指定)。ルーティング挙動は環境変数 `C3_TIER_COST_LAMBDA`(cost-weighted の重み・`0 ≤ λ ≤ 5`・v2.26.0〜、上限拡張 v2.27.0)/ `C3_TIER_EPSILON` / `C3_ESCALATION_THRESHOLD` で調整可([CLI リファレンス](https://satoh-y-0323.github.io/claude-code-conductor/cli-reference/)参照) |
158
158
 
159
159
  ### 基本的な使い方
160
160
 
@@ -1,3 +1,3 @@
1
1
  """Claude Code Conductor (C3) - multi-agent orchestration framework for Claude Code."""
2
2
 
3
- __version__ = "2.26.0"
3
+ __version__ = "2.28.0"
@@ -14,7 +14,6 @@ fast even when the embedding model has not yet been downloaded.
14
14
  from __future__ import annotations
15
15
 
16
16
  import argparse
17
- import hashlib
18
17
  import json
19
18
  import sys
20
19
  from pathlib import Path
@@ -30,6 +29,7 @@ from c3.recall_index import (
30
29
  RecallIndex,
31
30
  SourceChunk,
32
31
  collect_sources,
32
+ content_hash,
33
33
  default_index_paths,
34
34
  snippet_of,
35
35
  warn_if_stale,
@@ -200,6 +200,58 @@ def _handle_search(args: argparse.Namespace) -> int:
200
200
  return 0
201
201
 
202
202
 
203
+ def _partition_chunks_for_reuse(
204
+ chunks: list[SourceChunk],
205
+ old_index: RecallIndex,
206
+ ) -> tuple[list[int], dict[int, list[float]], int]:
207
+ """Partition *chunks* into embed targets and reusable vectors from *old_index*.
208
+
209
+ Returns a triple ``(to_embed_indices, reuse_map, reused_count)`` where:
210
+
211
+ - ``to_embed_indices``: positions in *chunks* that need fresh embedding
212
+ (new or content-changed chunks).
213
+ - ``reuse_map``: ``{chunk_position: vector}`` for unchanged chunks whose
214
+ vector was successfully retrieved from *old_index*.
215
+ - ``reused_count``: number of chunks successfully reused.
216
+
217
+ The caller is responsible for embedding ``to_embed_indices`` and then
218
+ assembling the final ``items`` list in original *chunks* order to keep
219
+ ``build()`` ID assignment consistent with a full rebuild.
220
+ """
221
+ # Build a lookup: (source_type, path, chunk_id) -> (source_hash, int_id)
222
+ old_key_map: dict[tuple[str, str, str], tuple[str, int]] = {}
223
+ for id_str, rec in old_index.meta.chunks.items():
224
+ key = (rec.source_type, rec.path, rec.chunk_id)
225
+ old_key_map[key] = (rec.source_hash, int(id_str))
226
+
227
+ # Partition chunks into those that need embedding and those that can be reused.
228
+ # We fill reuse_map for unchanged slots so the final items list preserves the
229
+ # original order — this ensures build() assigns sequential IDs identically to
230
+ # a full rebuild (same order → same IDs → same search results).
231
+ to_embed_indices: list[int] = [] # indices into chunks that need embedding
232
+ reuse_map: dict[int, list[float]] = {} # chunk index → reused vector
233
+ reused_count = 0
234
+
235
+ for i, src in enumerate(chunks):
236
+ key = (src.source_type, src.path, src.chunk_id)
237
+ new_hash = content_hash(src.content)
238
+ if key in old_key_map:
239
+ old_hash, old_id = old_key_map[key]
240
+ if old_hash == new_hash:
241
+ # Unchanged: reuse the stored vector.
242
+ try:
243
+ vec = old_index.get_vector(old_id)
244
+ reuse_map[i] = vec
245
+ reused_count += 1
246
+ continue
247
+ except Exception:
248
+ pass # Vector retrieval failed; fall through to embed
249
+ # New or changed chunk: needs embedding.
250
+ to_embed_indices.append(i)
251
+
252
+ return to_embed_indices, reuse_map, reused_count
253
+
254
+
203
255
  def _handle_rebuild(args: argparse.Namespace) -> int:
204
256
  repo_root = _resolve_repo_root(getattr(args, "target", None))
205
257
  if repo_root is None:
@@ -225,27 +277,98 @@ def _handle_rebuild(args: argparse.Namespace) -> int:
225
277
  )
226
278
  return 1
227
279
 
228
- print(f"[recall] embedding {len(chunks)} chunks...")
229
- vectors = embedder.embed_passages([c.content for c in chunks]) if chunks else []
280
+ index_path, meta_path = default_index_paths(repo_root)
281
+
282
+ # ----- incremental path -----
283
+ # Attempt to load the existing index and reuse vectors for unchanged chunks.
284
+ # Falls back to full embed when:
285
+ # - --force is requested
286
+ # - no existing index files
287
+ # - load() raises RuntimeError (model/dim mismatch or corrupt meta)
288
+ # - any other unexpected error during load
289
+
290
+ old_index: RecallIndex | None = None
291
+ if not args.force and index_path.exists() and meta_path.exists():
292
+ candidate = RecallIndex(
293
+ index_path=index_path,
294
+ meta_path=meta_path,
295
+ model_name=embedder.model_name,
296
+ dim=embedder.dim,
297
+ )
298
+ try:
299
+ candidate.load()
300
+ old_index = candidate
301
+ except Exception as exc:
302
+ # CR-E-002/SR-R-004: log the failure reason so operators can diagnose
303
+ # corrupt or mismatched index files. Exception message is intentionally
304
+ # omitted (type name only) to avoid leaking internal state — consistent
305
+ # with the SR-R-001 policy used in _hnsw_save/_hnsw_load.
306
+ print(
307
+ f"[recall] 既存 index を読めず増分不可・全再構築にフォールバック: {type(exc).__name__}",
308
+ file=sys.stderr,
309
+ )
310
+ old_index = None
311
+
230
312
  items: list[tuple[ChunkRecord, list[float]]] = []
231
- for src, vec in zip(chunks, vectors):
232
- # CR-M-04 / SR-L-5: compute source_hash from the full content so that
233
- # identical chunks in different files produce the same hash and changed
234
- # content is reliably detected across rebuilds.
235
- content_hash = hashlib.sha256(
236
- src.content.encode("utf-8", errors="replace")
237
- ).hexdigest()
238
- record = ChunkRecord(
239
- source_type=src.source_type,
240
- path=src.path,
241
- chunk_id=src.chunk_id,
242
- snippet=snippet_of(src.content),
243
- mtime=src.mtime,
244
- source_hash=content_hash,
313
+
314
+ if old_index is not None:
315
+ to_embed_indices, reuse_map, reused_count = _partition_chunks_for_reuse(
316
+ chunks, old_index
245
317
  )
246
- items.append((record, vec))
247
318
 
248
- index_path, meta_path = default_index_paths(repo_root)
319
+ # Embed only the changed/new chunks.
320
+ embed_count = len(to_embed_indices)
321
+ to_embed_contents = [chunks[i].content for i in to_embed_indices]
322
+ if to_embed_contents:
323
+ new_vectors = embedder.embed_passages(to_embed_contents)
324
+ else:
325
+ new_vectors = []
326
+
327
+ # i is always present in embed_vec_map because it was added to
328
+ # to_embed_indices during _partition_chunks_for_reuse — every chunk
329
+ # not in reuse_map is guaranteed to have an entry here.
330
+ embed_vec_map: dict[int, list[float]] = {
331
+ idx: vec for idx, vec in zip(to_embed_indices, new_vectors)
332
+ }
333
+
334
+ # Build items in original chunks order to preserve ID assignment consistency.
335
+ for i, src in enumerate(chunks):
336
+ if i in reuse_map:
337
+ vec = reuse_map[i]
338
+ else:
339
+ # Invariant: i is in to_embed_indices, so it is always present in
340
+ # embed_vec_map. Every chunk not placed in reuse_map during
341
+ # _partition_chunks_for_reuse was added to to_embed_indices and
342
+ # therefore has a corresponding entry in embed_vec_map.
343
+ vec = embed_vec_map[i]
344
+ record = ChunkRecord(
345
+ source_type=src.source_type,
346
+ path=src.path,
347
+ chunk_id=src.chunk_id,
348
+ snippet=snippet_of(src.content),
349
+ mtime=src.mtime,
350
+ source_hash=content_hash(src.content),
351
+ )
352
+ items.append((record, vec))
353
+
354
+ print(f"[recall] embedded {embed_count} / reused {reused_count} chunks")
355
+ else:
356
+ # Full embed path (--force, no prior index, or load failure).
357
+ print(f"[recall] embedding {len(chunks)} chunks...")
358
+ vectors = embedder.embed_passages([c.content for c in chunks]) if chunks else []
359
+ for src, vec in zip(chunks, vectors):
360
+ # CR-M-04 / SR-L-5: compute source_hash from the full content so that
361
+ # identical chunks in different files produce the same hash and changed
362
+ # content is reliably detected across rebuilds.
363
+ record = ChunkRecord(
364
+ source_type=src.source_type,
365
+ path=src.path,
366
+ chunk_id=src.chunk_id,
367
+ snippet=snippet_of(src.content),
368
+ mtime=src.mtime,
369
+ source_hash=content_hash(src.content),
370
+ )
371
+ items.append((record, vec))
249
372
 
250
373
  if args.force:
251
374
  for p in (index_path, meta_path):
@@ -145,6 +145,11 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
145
145
  "agent_cost": agent_cost,
146
146
  "tier_cost": tier_cost,
147
147
  "tier_cost_rate": tier_cost_rate,
148
+ "routing_params": {
149
+ "cost_lambda": c3_db.resolve_cost_lambda(),
150
+ "epsilon": c3_db.resolve_epsilon(),
151
+ "escalation_threshold": c3_db.resolve_escalation_threshold(),
152
+ },
148
153
  }
149
154
 
150
155
 
@@ -247,3 +252,18 @@ def _render_human(snapshot: dict[str, Any]) -> None:
247
252
  f"{row['rate_usd_per_mtok']:>18.4f}"
248
253
  )
249
254
  print()
255
+
256
+ print("== routing パラメータ(環境変数で調整可) ==")
257
+ rp = snapshot.get("routing_params", {})
258
+ cost_lambda = rp.get("cost_lambda")
259
+ epsilon = rp.get("epsilon", c3_db.EPSILON_TIEBREAK)
260
+ escalation_threshold = rp.get("escalation_threshold", c3_db.ESCALATION_THRESHOLD_DEFAULT)
261
+ if cost_lambda is None:
262
+ print("λ (C3_TIER_COST_LAMBDA): 未設定 → v2.25.0 互換(ε tie-break のみ)")
263
+ elif cost_lambda == 0.0:
264
+ print("λ: 0.0(cost 無視・純 Thompson)")
265
+ else:
266
+ print(f"λ: {cost_lambda}(全 tier weighting 有効)")
267
+ print(f"ε (C3_TIER_EPSILON): {epsilon}")
268
+ print(f"escalation threshold (C3_ESCALATION_THRESHOLD): {escalation_threshold}")
269
+ print()
@@ -18,8 +18,10 @@ upsert_po_status / fetch_po_status)も同時に削除した。
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
+ import math
21
22
  import os
22
23
  import sqlite3
24
+ import sys
23
25
  from datetime import datetime
24
26
  from pathlib import Path
25
27
 
@@ -51,6 +53,125 @@ COST_LAMBDA_DEFAULT = None
51
53
  # 本定数が SSOT(select_tier.py はここから参照)。
52
54
  ESCALATION_THRESHOLD_DEFAULT = 0.5
53
55
 
56
+ # cost-weighted Thompson の λ 有効範囲(v2.27.0: 上限を 1.0→5.0 に拡張)。
57
+ # cost を成功率より強く効かせる余地を確保するため上限を 5.0 に設定。
58
+ # select_tier.py の _resolve_cost_lambda はここを SSOT として参照する。
59
+ COST_LAMBDA_MIN = 0.0
60
+ COST_LAMBDA_MAX = 5.0
61
+
62
+
63
+ def resolve_cost_lambda() -> float | None:
64
+ """``C3_TIER_COST_LAMBDA`` を安全に解決する(cli_tier 用 SSOT)。
65
+
66
+ 不正値(非数値 / 0 未満 / COST_LAMBDA_MAX 超 / NaN)は受け付けず、
67
+ stderr 警告 + デフォルト(COST_LAMBDA_DEFAULT = None)に戻す。
68
+ 未設定 / 空文字は無警告でデフォルト(None)を返す。
69
+ 妥当域: [COST_LAMBDA_MIN, COST_LAMBDA_MAX](x=0 許容の閉区間)。
70
+ 戻り値が None の場合は v2.25.0 互換の ε tie-break 経路を維持する(センチネル)。
71
+ """
72
+ raw = os.environ.get("C3_TIER_COST_LAMBDA")
73
+ if raw is None or raw == "":
74
+ return COST_LAMBDA_DEFAULT
75
+ try:
76
+ x = float(raw)
77
+ except ValueError:
78
+ print(
79
+ f"[c3:cost_lambda] invalid C3_TIER_COST_LAMBDA={raw!r}, "
80
+ f"using default {COST_LAMBDA_DEFAULT}",
81
+ file=sys.stderr,
82
+ )
83
+ return COST_LAMBDA_DEFAULT
84
+ if math.isnan(x):
85
+ print(
86
+ f"[c3:cost_lambda] C3_TIER_COST_LAMBDA={raw!r} is NaN, "
87
+ f"using default {COST_LAMBDA_DEFAULT}",
88
+ file=sys.stderr,
89
+ )
90
+ return COST_LAMBDA_DEFAULT
91
+ if x < COST_LAMBDA_MIN or x > COST_LAMBDA_MAX:
92
+ print(
93
+ f"[c3:cost_lambda] C3_TIER_COST_LAMBDA={x!r} out of range "
94
+ f"[{COST_LAMBDA_MIN}, {COST_LAMBDA_MAX}], "
95
+ f"using default {COST_LAMBDA_DEFAULT}",
96
+ file=sys.stderr,
97
+ )
98
+ return COST_LAMBDA_DEFAULT
99
+ return x
100
+
101
+
102
+ def resolve_epsilon() -> float:
103
+ """``C3_TIER_EPSILON`` を安全に解決する(cli_tier 用 SSOT)。
104
+
105
+ 不正値(非数値 / 0 以下 / 1 超 / NaN)は受け付けず、
106
+ stderr 警告 + デフォルト(EPSILON_TIEBREAK)に戻す。
107
+ 未設定 / 空文字は無警告でデフォルトを返す。
108
+ 妥当域: (0, 1](x=0 拒否の半開区間)。
109
+ """
110
+ raw = os.environ.get("C3_TIER_EPSILON")
111
+ if raw is None or raw == "":
112
+ return EPSILON_TIEBREAK
113
+ try:
114
+ x = float(raw)
115
+ except ValueError:
116
+ print(
117
+ f"[c3:epsilon] invalid C3_TIER_EPSILON={raw!r}, "
118
+ f"using default {EPSILON_TIEBREAK}",
119
+ file=sys.stderr,
120
+ )
121
+ return EPSILON_TIEBREAK
122
+ if math.isnan(x):
123
+ print(
124
+ f"[c3:epsilon] C3_TIER_EPSILON={raw!r} is NaN, "
125
+ f"using default {EPSILON_TIEBREAK}",
126
+ file=sys.stderr,
127
+ )
128
+ return EPSILON_TIEBREAK
129
+ if x <= 0 or x > 1:
130
+ print(
131
+ f"[c3:epsilon] C3_TIER_EPSILON={x!r} out of range (0, 1], "
132
+ f"using default {EPSILON_TIEBREAK}",
133
+ file=sys.stderr,
134
+ )
135
+ return EPSILON_TIEBREAK
136
+ return x
137
+
138
+
139
+ def resolve_escalation_threshold() -> float:
140
+ """``C3_ESCALATION_THRESHOLD`` を安全に解決する(cli_tier 用 SSOT)。
141
+
142
+ 不正値(非数値 / 0 以下 / 1 超 / NaN)は受け付けず、
143
+ stderr 警告 + デフォルト(ESCALATION_THRESHOLD_DEFAULT)に戻す。
144
+ 未設定 / 空文字は無警告でデフォルトを返す。
145
+ 妥当域: (0, 1](x=0 拒否の半開区間)。
146
+ """
147
+ raw = os.environ.get("C3_ESCALATION_THRESHOLD")
148
+ if raw is None or raw == "":
149
+ return ESCALATION_THRESHOLD_DEFAULT
150
+ try:
151
+ x = float(raw)
152
+ except ValueError:
153
+ print(
154
+ f"[c3:escalation] invalid C3_ESCALATION_THRESHOLD={raw!r}, "
155
+ f"using default {ESCALATION_THRESHOLD_DEFAULT}",
156
+ file=sys.stderr,
157
+ )
158
+ return ESCALATION_THRESHOLD_DEFAULT
159
+ if math.isnan(x):
160
+ print(
161
+ f"[c3:escalation] C3_ESCALATION_THRESHOLD={raw!r} is NaN, "
162
+ f"using default {ESCALATION_THRESHOLD_DEFAULT}",
163
+ file=sys.stderr,
164
+ )
165
+ return ESCALATION_THRESHOLD_DEFAULT
166
+ if x <= 0 or x > 1:
167
+ print(
168
+ f"[c3:escalation] C3_ESCALATION_THRESHOLD={x!r} out of range (0, 1], "
169
+ f"using default {ESCALATION_THRESHOLD_DEFAULT}",
170
+ file=sys.stderr,
171
+ )
172
+ return ESCALATION_THRESHOLD_DEFAULT
173
+ return x
174
+
54
175
 
55
176
  def _apply_busy_timeout(conn: sqlite3.Connection) -> None:
56
177
  # PRAGMA はパラメータバインドできないため値が整数であることを int() で強制する。
@@ -159,9 +159,7 @@ class RecallIndex:
159
159
  # If the caller already computed a hash (e.g. from full content),
160
160
  # keep it; otherwise derive from the stored snippet as a fallback.
161
161
  if not record.source_hash:
162
- record.source_hash = hashlib.sha256(
163
- record.snippet.encode("utf-8", errors="replace")
164
- ).hexdigest()
162
+ record.source_hash = content_hash(record.snippet)
165
163
  self._meta.chunks[str(new_id)] = record
166
164
  self._index.add_items(vecs, ids)
167
165
  self._meta.rebuilt_at = _utcnow_iso()
@@ -293,6 +291,22 @@ class RecallIndex:
293
291
  def chunk_count(self) -> int:
294
292
  return len(self._meta.chunks)
295
293
 
294
+ def get_vector(self, chunk_id: int) -> list[float]:
295
+ """Return the stored vector for ``chunk_id`` as ``list[float]``.
296
+
297
+ Raises ``RuntimeError`` if the index has not been built or loaded yet.
298
+ Propagates hnswlib's exception (typically ``RuntimeError`` or
299
+ ``IndexError``) when ``chunk_id`` is not present in the index.
300
+ """
301
+ if self._index is None:
302
+ raise RuntimeError(
303
+ "index has not been built or loaded; call build() or load() first"
304
+ )
305
+ # hnswlib.get_items returns a 2-D array of shape (n, dim).
306
+ # We pass a single-element list and take the first row.
307
+ result = self._index.get_items([chunk_id])
308
+ return [float(x) for x in result[0]]
309
+
296
310
  # ----- internals -----
297
311
 
298
312
  def _new_index(self, *, max_elements: int):
@@ -475,6 +489,17 @@ def is_stale(repo_root: Path, index_path: Path) -> bool:
475
489
  # ----- helpers -----
476
490
 
477
491
 
492
+ def content_hash(text: str) -> str:
493
+ """Return the SHA-256 hex digest of *text* encoded as UTF-8.
494
+
495
+ ``errors="replace"`` ensures arbitrary Unicode input never raises.
496
+ This is the canonical hash used for incremental-rebuild change detection
497
+ (``cli_recall._handle_rebuild``) and as the ``source_hash`` fallback in
498
+ :meth:`RecallIndex.build`.
499
+ """
500
+ return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
501
+
502
+
478
503
  def snippet_of(text: str, *, max_chars: int = SNIPPET_CHARS) -> str:
479
504
  """Return a stable preview of ``text`` for storage / display."""
480
505
  text = (text or "").strip()