claude-code-conductor 2.21.0__tar.gz → 2.23.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.
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/select_tier.py +154 -11
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/dev-workflow/scripts/record_tier_outcome.py +5 -1
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/CHANGELOG.md +52 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/PKG-INFO +1 -1
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/__init__.py +1 -1
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_tier.py +20 -2
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/db.py +138 -3
- claude_code_conductor-2.23.0/src/c3/migrations/003_tier_cost.sql +25 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/pricing.py +41 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_record_tier_outcome.py +115 -0
- claude_code_conductor-2.23.0/tests/hooks/test_select_tier.py +1005 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_tier.py +164 -5
- claude_code_conductor-2.23.0/tests/test_db.py +664 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_migrate.py +151 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_pricing.py +46 -1
- claude_code_conductor-2.21.0/tests/hooks/test_select_tier.py +0 -413
- claude_code_conductor-2.21.0/tests/test_db.py +0 -320
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/CLAUDE.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/architect.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/code-reviewer.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/developer.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/doc-writer.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/interviewer.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/planner.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/project-setup.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/security-reviewer.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/systematic-debugger.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/tester.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/wt_developer.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/wt_systematic-debugger.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/agents/wt_tester.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/breaking-changes.txt +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/deletions.txt +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/docs/config-policy.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/docs/parallel-agents-setup.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/docs/platform-adapters.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/docs/settings.json.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/_hook_utils.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/check_agent_invocation.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/consolidate_memory.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/permission_handler.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/permission_handler_toast.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/planner_check.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/post_tool.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/pre_compact.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/pre_tool.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/recall_inject.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/restore_session.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/session_start.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/session_stop.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/session_utils.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/statusline.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/stop.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/hooks/worktree_guard.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/memory/.gitkeep +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/permission_rules.json +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/rules/promoted/index.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/settings.json +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/codex-review/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/dev-workflow/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/develop/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/doc/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/extract-lib/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/init-session/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/mcp-config/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/parallel-agents/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/pattern-status/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/recall/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/review-phase/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/setup/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/setup/reference.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/skills/start/SKILL.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.claude/state/.gitkeep +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/.gitignore +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/LICENSE +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/LICENSES/chroma-hnswlib-LICENSE +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/LICENSES/chroma-hnswlib-NOTICE +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/LICENSES/fastembed-LICENSE +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/LICENSES/fastembed-NOTICE +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/LICENSES/onnxruntime-LICENSE +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/README.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/hatch_build.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/pyproject.toml +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/__main__.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/_excludes.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/_terminal.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/adapters.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_ask.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_doctor.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_init.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_list.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_plan.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_recall.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/cli_update.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/embedding.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/mcp_server.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/migrate.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/migrations/001_initial.sql +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/migrations/002_agent_cost_runs.sql +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/migrations/README.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/migrations/__init__.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/paths.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/plan_validator.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/platforms.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/question.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/recall_chunker.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/recall_index.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/src/c3/usage_ingester.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/__init__.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/conftest.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/fixtures/usage/README.md +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/fixtures/usage/mainline.jsonl +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/__init__.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_check_agent_invocation.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_consolidate_memory.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_hook_utils.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_permission_handler.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_permission_handler_toast.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_planner_check.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_planner_check_dev.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_post_tool.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_pre_tool.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_recall_inject.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_record_review_decision.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_restore_session.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_review_hint_inject.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_select_tier_escalation.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_session_start.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_session_stop.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_session_utils.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_similarity_boost.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_statusline.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_statusline_template_sync.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_sync_check.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/hooks/test_template_guard.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/__init__.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/_skill_helpers.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_init_session_no_task_type.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_planner_lightweight.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_recall_skill.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_setup_templates.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_start_skill_new_flow.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_adapters.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_ask.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_entry.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_init.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_list.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_plan.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_recall.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_update_breaking_changes.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_cli_update_deletions.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_docstring_consistency.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_embedding.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_excludes.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_extract_breaking_changes.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_mcp_server_elicit.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_paths.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_plan_validator.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_pre_compact.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_precompact_additional.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_precompact_toctou_fixes.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_recall_chunker.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_recall_index.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_references_migration.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_session_utils_additional.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_skill_no_builtin_conflict.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_statusline.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_stop_additional.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_stop_hook.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_stop_precompact_fixes.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_sync_template_stop.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_template_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_usage_ingester.py +0 -0
- {claude_code_conductor-2.21.0 → claude_code_conductor-2.23.0}/tests/test_worktree_guard.py +0 -0
|
@@ -31,6 +31,7 @@ import os
|
|
|
31
31
|
import random
|
|
32
32
|
import re
|
|
33
33
|
import sys
|
|
34
|
+
from typing import NamedTuple
|
|
34
35
|
from pathlib import Path
|
|
35
36
|
|
|
36
37
|
try:
|
|
@@ -110,6 +111,69 @@ _PROMPT_HISTORY_SCAN_LINES = 1000
|
|
|
110
111
|
|
|
111
112
|
TIERS: tuple[str, ...] = ("haiku", "sonnet", "opus")
|
|
112
113
|
|
|
114
|
+
# cost-aware tie-break の拮抗判定閾値(v2.23.0)。
|
|
115
|
+
# Beta サンプルは 0〜1 スケール。成功率 5pt 以内=実質同等とみなす拮抗判定閾値。
|
|
116
|
+
# 過大は成功率犠牲リスク、過小は無発動。調整可能化は v2.24.0。
|
|
117
|
+
EPSILON: float = 0.05
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SelectionResult(NamedTuple):
|
|
121
|
+
"""select_tier_detailed の戻り値(NamedTuple = immutable)。
|
|
122
|
+
|
|
123
|
+
tier: 選択された tier 名。
|
|
124
|
+
mode: "uniform" または "thompson"。
|
|
125
|
+
cost_tiebreak: Thompson 分岐で cost tie-break が発動した場合 True。
|
|
126
|
+
contenders: 拮抗判定に入った tier のタプル(observability/デバッグ用)。
|
|
127
|
+
frozen 安全のため list ではなく tuple を使用。
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
tier: str
|
|
131
|
+
mode: str
|
|
132
|
+
cost_tiebreak: bool = False
|
|
133
|
+
contenders: tuple[str, ...] = ()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _cost_tiebreak(
|
|
137
|
+
samples: dict[str, float],
|
|
138
|
+
cost_map: dict[str, float] | None,
|
|
139
|
+
*,
|
|
140
|
+
epsilon: float = EPSILON,
|
|
141
|
+
) -> tuple[str, bool, tuple[str, ...]]:
|
|
142
|
+
"""Thompson サンプル拮抗群内で min-max 正規化コストが最安の tier を返す。
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
samples: {tier: beta_sample} の dict(Thompson Sampling 結果)。
|
|
146
|
+
cost_map: {tier: cost} の dict。None なら cost を見ず従来挙動。
|
|
147
|
+
cost は実測 avg_cost_usd または静的参照単価(ハイブリッド)。
|
|
148
|
+
混在スケール(USD vs per-MTok)の厳密化は v2.24.0。
|
|
149
|
+
``cost_map`` は None、または contenders 全件をキーとして含む dict を
|
|
150
|
+
渡すこと。partial dict を渡すと ``cost_map[t]`` で KeyError が発生する。
|
|
151
|
+
``select_tier_detailed`` 経由では呼び出し側(main)が全 TIERS 分を
|
|
152
|
+
構築して保証する。
|
|
153
|
+
epsilon: 拮抗判定の閾値(デフォルト EPSILON=0.05)。
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
(chosen, did_tiebreak, contenders) のタプル。
|
|
157
|
+
- chosen: 選択された tier 名。
|
|
158
|
+
- did_tiebreak: cost tie-break が発動した場合 True。
|
|
159
|
+
- contenders: 拮抗判定に入った tier のタプル。
|
|
160
|
+
"""
|
|
161
|
+
max_sample = max(samples.values())
|
|
162
|
+
contenders = [t for t in samples if max_sample - samples[t] <= epsilon]
|
|
163
|
+
|
|
164
|
+
if len(contenders) <= 1 or cost_map is None:
|
|
165
|
+
# 従来挙動と完全一致: max(samples, key=lambda t: samples[t]) と同じ式
|
|
166
|
+
chosen = max(samples, key=lambda t: samples[t])
|
|
167
|
+
return chosen, False, tuple(contenders)
|
|
168
|
+
|
|
169
|
+
# 拮抗群内で min-max 正規化コストを計算し最安 tier を選ぶ
|
|
170
|
+
costs = {t: cost_map[t] for t in contenders}
|
|
171
|
+
lo, hi = min(costs.values()), max(costs.values())
|
|
172
|
+
norm = {t: ((costs[t] - lo) / (hi - lo) if hi > lo else 0.0) for t in contenders}
|
|
173
|
+
# 同値安定 tie-break: norm 同値時はサンプル大(=従来選好)を優先 → 決定論
|
|
174
|
+
chosen = min(contenders, key=lambda t: (norm[t], -samples[t]))
|
|
175
|
+
return chosen, True, tuple(contenders)
|
|
176
|
+
|
|
113
177
|
|
|
114
178
|
def _prompt_prefix_and_hash(prompt: str) -> tuple[str, str]:
|
|
115
179
|
"""prompt から (prefix, hash) を抽出する。
|
|
@@ -226,34 +290,67 @@ def estimate_complexity(prompt: str) -> str:
|
|
|
226
290
|
return "medium"
|
|
227
291
|
|
|
228
292
|
|
|
229
|
-
def
|
|
293
|
+
def select_tier_detailed(
|
|
230
294
|
params: dict[str, tuple[float, float, int]],
|
|
231
295
|
*,
|
|
232
296
|
rng: random.Random | None = None,
|
|
233
|
-
|
|
234
|
-
|
|
297
|
+
cost_map: dict[str, float] | None = None,
|
|
298
|
+
) -> SelectionResult:
|
|
299
|
+
"""Beta サンプリングまたは uniform 選択で推奨 Tier を SelectionResult で返す。
|
|
235
300
|
|
|
236
301
|
Args:
|
|
237
302
|
params: ``read_tier_params`` の戻り値。
|
|
238
303
|
``{"haiku": (alpha, beta, trials), ...}``
|
|
239
304
|
rng: テスト用に決定論的にしたい場合は ``random.Random(seed)`` を渡す。
|
|
305
|
+
cost_map: {tier: cost} の dict、または None。
|
|
306
|
+
None(cost を見ない=従来 Thompson)または「params の全 tier キーを
|
|
307
|
+
含む完全な dict」のいずれか。partial dict は渡されない前提
|
|
308
|
+
(呼び出し側が全 TIERS 分を構築して保証する)。
|
|
309
|
+
uniform 分岐では cost_map の有無に関わらず完全無視する(探索保護)。
|
|
240
310
|
|
|
241
311
|
Returns:
|
|
242
|
-
|
|
243
|
-
|
|
312
|
+
SelectionResult(tier, mode, cost_tiebreak, contenders)。
|
|
313
|
+
mode は ``"thompson"`` / ``"uniform"``。
|
|
244
314
|
"""
|
|
245
315
|
rng = rng or random
|
|
246
316
|
total_trials = sum(p[2] for p in params.values())
|
|
247
317
|
if total_trials < LEARNING_THRESHOLD:
|
|
248
|
-
|
|
318
|
+
# uniform: cost を完全無視・従来挙動完全維持
|
|
319
|
+
return SelectionResult(rng.choice(TIERS), "uniform", False, ())
|
|
249
320
|
|
|
250
|
-
#
|
|
321
|
+
# Thompson Sampling: rng の消費順序を従来 select_tier と完全一致させる
|
|
251
322
|
samples = {
|
|
252
323
|
tier: rng.betavariate(p[0], p[1])
|
|
253
324
|
for tier, p in params.items()
|
|
254
325
|
}
|
|
255
|
-
chosen =
|
|
256
|
-
return chosen, "thompson"
|
|
326
|
+
chosen, did_tiebreak, contenders = _cost_tiebreak(samples, cost_map)
|
|
327
|
+
return SelectionResult(chosen, "thompson", did_tiebreak, contenders)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def select_tier(
|
|
331
|
+
params: dict[str, tuple[float, float, int]],
|
|
332
|
+
*,
|
|
333
|
+
rng: random.Random | None = None,
|
|
334
|
+
cost_map: dict[str, float] | None = None,
|
|
335
|
+
) -> tuple[str, str]:
|
|
336
|
+
"""Beta サンプリングまたは uniform 選択で推奨 Tier を返す。
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
params: ``read_tier_params`` の戻り値。
|
|
340
|
+
``{"haiku": (alpha, beta, trials), ...}``
|
|
341
|
+
rng: テスト用に決定論的にしたい場合は ``random.Random(seed)`` を渡す。
|
|
342
|
+
cost_map: {tier: cost} の dict、または None。
|
|
343
|
+
None なら cost を見ず従来の Thompson Sampling と完全一致。
|
|
344
|
+
uniform 分岐では cost_map の有無に関わらず完全無視する。
|
|
345
|
+
詳細は :func:`select_tier_detailed` を参照。
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
``(tier, mode)`` のタプル。``mode`` は ``"thompson"`` / ``"uniform"`` で、
|
|
349
|
+
プロンプトに「学習データ収集中」と表示するかの分岐に使う。
|
|
350
|
+
戻り値型は v2.22.0 以前と完全に不変。
|
|
351
|
+
"""
|
|
352
|
+
result = select_tier_detailed(params, rng=rng, cost_map=cost_map)
|
|
353
|
+
return result.tier, result.mode
|
|
257
354
|
|
|
258
355
|
|
|
259
356
|
# Phase 2-B: 失敗率による昇格マッピング(haiku → sonnet, sonnet → opus)。
|
|
@@ -323,6 +420,8 @@ def write_tier_selection(
|
|
|
323
420
|
escalation_reason: str | None = None,
|
|
324
421
|
prompt_prefix: str | None = None,
|
|
325
422
|
prompt_hash: str | None = None,
|
|
423
|
+
session_id: str | None = None,
|
|
424
|
+
cost_tiebreak: bool = False,
|
|
326
425
|
) -> None:
|
|
327
426
|
"""直近の選択結果を ``tier_selection.json`` に書く。
|
|
328
427
|
|
|
@@ -335,6 +434,13 @@ def write_tier_selection(
|
|
|
335
434
|
|
|
336
435
|
``escalated`` / ``escalation_reason`` を任意で含める。
|
|
337
436
|
failure rate に基づく昇格が起きた場合のみ True / 文字列が入る。
|
|
437
|
+
|
|
438
|
+
``session_id`` を任意で含める。UserPromptSubmit payload の session UUID。
|
|
439
|
+
None のときは tier_selection.json のキー自体を省略する(後方互換)。
|
|
440
|
+
|
|
441
|
+
``cost_tiebreak`` を任意で含める(v2.23.0)。
|
|
442
|
+
Thompson Sampling の拮抗群内で cost tie-break が発動した場合のみ True。
|
|
443
|
+
False のときはキー自体を省略する(escalated/session_id と同パターン)。
|
|
338
444
|
"""
|
|
339
445
|
os.makedirs(os.path.dirname(TIER_SELECTION_PATH), exist_ok=True)
|
|
340
446
|
payload: dict[str, object] = {
|
|
@@ -354,6 +460,10 @@ def write_tier_selection(
|
|
|
354
460
|
payload["prompt_prefix"] = prompt_prefix
|
|
355
461
|
if prompt_hash is not None:
|
|
356
462
|
payload["prompt_hash"] = prompt_hash
|
|
463
|
+
if session_id is not None:
|
|
464
|
+
payload["session_id"] = session_id
|
|
465
|
+
if cost_tiebreak:
|
|
466
|
+
payload["cost_tiebreak"] = True
|
|
357
467
|
try:
|
|
358
468
|
with open(TIER_SELECTION_PATH, "w", encoding="utf-8") as f:
|
|
359
469
|
json.dump(payload, f, ensure_ascii=False)
|
|
@@ -370,8 +480,13 @@ def build_additional_context(
|
|
|
370
480
|
*,
|
|
371
481
|
escalation_reason: str | None = None,
|
|
372
482
|
complexity_source: str | None = None,
|
|
483
|
+
cost_tiebreak: bool = False,
|
|
373
484
|
) -> str:
|
|
374
|
-
"""親 Claude に追加注入する文字列を組み立てる。
|
|
485
|
+
"""親 Claude に追加注入する文字列を組み立てる。
|
|
486
|
+
|
|
487
|
+
``cost_tiebreak`` が True のとき、suffix に cost-aware 発動を示す文言を追加する(v2.23.0)。
|
|
488
|
+
False のときは不変(既存文言と完全一致)。
|
|
489
|
+
"""
|
|
375
490
|
trials = sum(p[2] for p in params.values())
|
|
376
491
|
if mode == "uniform":
|
|
377
492
|
confidence = f"学習データ収集中(合計 {trials}/{LEARNING_THRESHOLD} 試行)"
|
|
@@ -384,6 +499,8 @@ def build_additional_context(
|
|
|
384
499
|
suffix += f" [Phase 2-B 昇格: {escalation_reason}]"
|
|
385
500
|
if complexity_source:
|
|
386
501
|
suffix += f" [複雑度判定: {complexity_source}]"
|
|
502
|
+
if cost_tiebreak:
|
|
503
|
+
suffix += " [cost-aware: 成功率拮抗のため低コスト Tier を選択]"
|
|
387
504
|
|
|
388
505
|
return (
|
|
389
506
|
f"[tier-routing 推奨] 複雑度: {complexity} / 推奨 Tier: {tier}({confidence})。"
|
|
@@ -415,6 +532,8 @@ def main() -> int:
|
|
|
415
532
|
if not isinstance(prompt, str) or not prompt.strip():
|
|
416
533
|
return 0
|
|
417
534
|
|
|
535
|
+
session_id = payload.get("session_id")
|
|
536
|
+
|
|
418
537
|
# Phase 2-C: 類似度推定で complexity を補強
|
|
419
538
|
heuristic_complexity = estimate_complexity(prompt)
|
|
420
539
|
strong_complexity, weak_matches = similarity_boost(prompt)
|
|
@@ -437,7 +556,28 @@ def main() -> int:
|
|
|
437
556
|
else:
|
|
438
557
|
params = c3_db.read_tier_params(complexity)
|
|
439
558
|
|
|
440
|
-
|
|
559
|
+
# v2.23.0: cost_map をハイブリッド解決(実測 avg_cost を主に、欠損 tier は静的単価で補完)。
|
|
560
|
+
# c3_db が None または pricing import 失敗時は cost_map=None で従来 Thompson にデグレード。
|
|
561
|
+
# cost_map=None の場合、select_tier_detailed/_cost_tiebreak は従来 Thompson 挙動と完全一致(引数定義参照)。
|
|
562
|
+
cost_map = None
|
|
563
|
+
if c3_db is not None:
|
|
564
|
+
try:
|
|
565
|
+
from c3 import pricing # type: ignore[import-not-found]
|
|
566
|
+
measured = c3_db.read_tier_cost_for_complexity(complexity) # {tier: avg} 実測>0 のみ
|
|
567
|
+
cost_map = {}
|
|
568
|
+
for tier_name in TIERS:
|
|
569
|
+
if tier_name in measured and measured[tier_name] > 0:
|
|
570
|
+
cost_map[tier_name] = measured[tier_name]
|
|
571
|
+
else:
|
|
572
|
+
# tier_reference_cost は未知 tier に 0.0 を返すが、TIERS と _TIER_REFERENCE_KEY は
|
|
573
|
+
# 同期前提のため現行 3 tier(haiku/sonnet/opus)では 0.0 混入は起きない。
|
|
574
|
+
cost_map[tier_name] = pricing.tier_reference_cost(tier_name) # 静的 fallback
|
|
575
|
+
except ImportError:
|
|
576
|
+
cost_map = None
|
|
577
|
+
|
|
578
|
+
result = select_tier_detailed(params, cost_map=cost_map)
|
|
579
|
+
tier, mode = result.tier, result.mode
|
|
580
|
+
cost_tiebreak = result.cost_tiebreak
|
|
441
581
|
|
|
442
582
|
# Phase 2-B: failure rate に基づく escalation
|
|
443
583
|
effective_tier, escalation_reason = maybe_escalate(complexity, tier)
|
|
@@ -448,12 +588,15 @@ def main() -> int:
|
|
|
448
588
|
escalated=escalated, escalation_reason=escalation_reason,
|
|
449
589
|
prompt_prefix=prompt_prefix,
|
|
450
590
|
prompt_hash=prompt_hash,
|
|
591
|
+
session_id=session_id,
|
|
592
|
+
cost_tiebreak=cost_tiebreak,
|
|
451
593
|
)
|
|
452
594
|
|
|
453
595
|
context_text = build_additional_context(
|
|
454
596
|
complexity, effective_tier, mode, params,
|
|
455
597
|
escalation_reason=escalation_reason,
|
|
456
598
|
complexity_source=complexity_source,
|
|
599
|
+
cost_tiebreak=cost_tiebreak,
|
|
457
600
|
)
|
|
458
601
|
output = {
|
|
459
602
|
"hookSpecificOutput": {
|
|
@@ -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,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.23.0] - 2026-05-25
|
|
4
|
+
|
|
5
|
+
**tier-routing cost-aware tie-break**: `select_tier` の Thompson Sampling 分岐に「拮抗 tier 群内コスト tie-break」を追加する。サンプル最大から ε(=0.05) 以内の拮抗 tier が複数ある場合のみ、min-max 正規化コストが最安の tier を選ぶ。単独最大なら従来通り(挙動不変)。成功率を犠牲にしない最小スコープの cost 統合。
|
|
6
|
+
|
|
7
|
+
**スコープ注記**:
|
|
8
|
+
- **最小スコープ=tie-break のみ**。精度向上(model 一致集計・agent_id 単位紐づけ)と `tier_bandit.total_cost_usd`/`cost_samples` への書き込みは **v2.24.0**。
|
|
9
|
+
- cost は**ハイブリッド源**(実測 avg_cost を主・欠損 tier は静的参照単価で補完)。実測 USD と静的 per-MTok の混在スケールは min-max 正規化で**拮抗群内の概算順位**に畳む(厳密な単位整合は v2.24.0)。
|
|
10
|
+
- LEARNING_THRESHOLD(30) 未満は cost 完全無視(uniform のまま・探索保護)。escalation は不変。
|
|
11
|
+
- migration なし(読み出しのみ・書き込みなし)。**破壊的変更なし**(optional kw-only 引数追加・None/False 時従来動作・出力キー条件付き追加のみ)。
|
|
12
|
+
|
|
13
|
+
### 機能追加
|
|
14
|
+
|
|
15
|
+
- **`src/c3/pricing.py`: `tier_reference_cost(tier) -> float`(新規)**: tier 名から input+output 静的参照単価和を返す純関数。haiku < sonnet < opus の単調性を保証。未知 tier は 0.0 を返す。`_TIER_REFERENCE_KEY` 定数で TIERS との同期チェックを明示。
|
|
16
|
+
|
|
17
|
+
- **`src/c3/db.py`: `read_tier_cost_for_complexity(complexity, *, db_path=None) -> dict[str, float]`(新規)**: `read_tier_cost_summary` を complexity 一致・avg_cost_usd > 0 でフィルタし `{tier: avg_cost_usd}` を返す薄いラッパー。テーブル不在・データ不在・DB 不在で `{}`。
|
|
18
|
+
|
|
19
|
+
- **`.claude/hooks/select_tier.py`**: `EPSILON=0.05` 定数 / `SelectionResult`(NamedTuple: tier/mode/cost_tiebreak/contenders)/ `_cost_tiebreak`(拮抗群 min-max 最安・同値はサンプル大優先で決定論)/ `select_tier_detailed(params, *, rng, cost_map)`(2 層 API の詳細版)を追加。
|
|
20
|
+
|
|
21
|
+
### 変更
|
|
22
|
+
|
|
23
|
+
- **`.claude/hooks/select_tier.py`**: `select_tier` に kw-only `cost_map=None` を追加し `select_tier_detailed` への委譲に変更(**戻り値型 (tier, mode) 不変**)。`write_tier_selection`/`build_additional_context` に `cost_tiebreak: bool = False` を追加(True 時のみ json キー追加 / context suffix「[cost-aware: 成功率拮抗のため低コスト Tier を選択]」追記)。`main()` で cost_map をハイブリッド解決し `select_tier_detailed` を使用(c3_db/pricing import 失敗時は cost_map=None で従来 Thompson にデグレード)。
|
|
24
|
+
|
|
25
|
+
- **`src/c3/__init__.py`**: `__version__` を `"2.22.0"` から `"2.23.0"` に更新。
|
|
26
|
+
|
|
27
|
+
- **注記更新**: `c3 tier stats` の「精度向上は v2.23.0」を「精度向上は v2.24.0」へ、「cost-aware routing 本体は v2.23.0 予定」を「cost-aware routing 本体(tie-break)実装済み。精度向上は v2.24.0 予定」へ更新。
|
|
28
|
+
|
|
29
|
+
## [2.22.0] - 2026-05-25
|
|
30
|
+
|
|
31
|
+
**tier-routing cost 紐づけデータ蓄積**: tier_recent_outcomes に session_id 列を追加し、agent_cost_runs と JOIN できるデータ基盤を整備する。`c3 tier stats` に complexity×tier 別の平均コストセクションを追加する。cost-aware routing 本体は v2.23.0。
|
|
32
|
+
|
|
33
|
+
**スコープ注記**: 単価テーブル自動更新は公式 price API が非提供のため pricing.py の手動メンテ継続。tier_bandit の total_cost_usd / cost_samples 列は v2.23.0 用に確保のみ(書き込み・読み出しなし)。
|
|
34
|
+
|
|
35
|
+
### 機能追加
|
|
36
|
+
|
|
37
|
+
- **`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 で保持(破壊的変更なし)。
|
|
38
|
+
|
|
39
|
+
- **`src/c3/db.py`: `record_tier_recent_outcome` に `session_id` 追加**: kw-only 引数 `session_id: str | None = None` を追加。既存呼び出しは後方互換(省略時 NULL 保存)。
|
|
40
|
+
|
|
41
|
+
- **`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 で `[]`。
|
|
42
|
+
|
|
43
|
+
- **`.claude/hooks/select_tier.py`: session_id 記録**: UserPromptSubmit payload から session_id を取得し tier_selection.json に追記。session_id が None のときはキーを省略(既存テスト互換)。
|
|
44
|
+
|
|
45
|
+
- **`.claude/skills/dev-workflow/scripts/record_tier_outcome.py`: session_id 受け渡し**: tier_selection.json から session_id を読み `record_tier_recent_outcome` に渡す。session_id キーが無い古い json でも動作(None として扱う)。
|
|
46
|
+
|
|
47
|
+
- **`c3 tier stats` に Tier 別平均コストセクション追加**: `_collect_snapshot()` に `read_tier_cost_summary()` の結果を `tier_cost` キーで追加。human 表示に「Tier 別平均コスト(粗い概算 / 精度向上は v2.23.0)」セクションを追加。tier_cost が空のときは「(cost 紐づけデータ未収集)」と表示。`--json` 出力の `tier_cost` キーにも自動反映。
|
|
48
|
+
|
|
49
|
+
### 変更
|
|
50
|
+
|
|
51
|
+
- **`src/c3/__init__.py`**: `__version__` を `"2.21.0"` から `"2.22.0"` に更新。
|
|
52
|
+
|
|
53
|
+
- **注記更新**: `c3 tier stats` の「cost-aware routing は v2.22.0 予定」を「データ紐づけ蓄積。cost-aware routing 本体は v2.23.0 予定」へ更新。
|
|
54
|
+
|
|
3
55
|
## [2.21.0] - 2026-05-25
|
|
4
56
|
|
|
5
57
|
**tier-routing コスト統合(データ収集基盤)**: Claude Code セッションログ(`~/.claude/projects/<slug>/<session>.jsonl` + subagent jsonl)を読み込み、モデル単価で USD 換算して c3.db に蓄積するデータ収集基盤を整備する。将来の cost-aware routing(v2.22.0)の土台。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-conductor
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.23.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
|
|
@@ -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 / agent_cost を読み 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
|
|
|
@@ -116,6 +116,8 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
|
|
|
116
116
|
|
|
117
117
|
agent_cost: list[dict[str, Any]] = c3_db.read_agent_cost_summary(db_path=db_path)
|
|
118
118
|
|
|
119
|
+
tier_cost: list[dict[str, Any]] = c3_db.read_tier_cost_summary(db_path=db_path)
|
|
120
|
+
|
|
119
121
|
if total_trials < _LEARNING_THRESHOLD:
|
|
120
122
|
mode = "uniform"
|
|
121
123
|
else:
|
|
@@ -130,6 +132,7 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
|
|
|
130
132
|
"tier_bandit": bandit_rows,
|
|
131
133
|
"recent_outcomes": recent_outcomes,
|
|
132
134
|
"agent_cost": agent_cost,
|
|
135
|
+
"tier_cost": tier_cost,
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
|
|
@@ -190,4 +193,19 @@ def _render_human(snapshot: dict[str, Any]) -> None:
|
|
|
190
193
|
f"{note}"
|
|
191
194
|
)
|
|
192
195
|
print()
|
|
193
|
-
|
|
196
|
+
|
|
197
|
+
print("== Tier 別平均コスト(粗い概算 / 精度向上は v2.24.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 本体(tie-break)実装済み。精度向上は v2.24.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
|
|
|
@@ -657,6 +666,132 @@ def read_agent_cost_summary(
|
|
|
657
666
|
]
|
|
658
667
|
|
|
659
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 read_tier_cost_for_complexity(
|
|
763
|
+
complexity: str,
|
|
764
|
+
*,
|
|
765
|
+
db_path: Path | None = None,
|
|
766
|
+
) -> dict[str, float]:
|
|
767
|
+
"""complexity 別の tier 平均コストを {tier: avg_cost_usd} で返す。
|
|
768
|
+
|
|
769
|
+
tie-break のハイブリッド cost 源(実測 avg_cost)。
|
|
770
|
+
complexity 一致 & avg_cost_usd > 0 のみ。
|
|
771
|
+
``read_tier_cost_summary`` の薄いラッパー。
|
|
772
|
+
|
|
773
|
+
DB アクセスは ``read_tier_cost_summary`` に委譲するため、
|
|
774
|
+
DB 例外処理・busy_timeout・read 規約は同関数から継承される。
|
|
775
|
+
データ/DB 不在で ``read_tier_cost_summary`` が ``[]`` を返す場合、
|
|
776
|
+
本関数は ``{}`` を返す。
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
complexity: フィルタ対象の complexity 値("simple" / "medium" / "complex")。
|
|
780
|
+
db_path: c3.db のパス。省略時は :func:`locate_c3_db` で探索
|
|
781
|
+
(``read_tier_cost_summary`` に委譲)。
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
``{tier: avg_cost_usd}`` の dict。
|
|
785
|
+
該当データ不在・DB 不在・エラー時は ``{}``。
|
|
786
|
+
"""
|
|
787
|
+
rows = read_tier_cost_summary(db_path=db_path)
|
|
788
|
+
return {
|
|
789
|
+
r["tier"]: r["avg_cost_usd"]
|
|
790
|
+
for r in rows
|
|
791
|
+
if r["complexity"] == complexity and r["avg_cost_usd"] > 0
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
|
|
660
795
|
def get_ingest_offset(
|
|
661
796
|
file_key: str,
|
|
662
797
|
*,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- C3 SQLite migration 003: tier_recent_outcomes に session_id 列・INDEX を追加し、
|
|
2
|
+
-- tier_bandit にコスト集計用列(v2.23.0 用)を確保する。
|
|
3
|
+
--
|
|
4
|
+
-- 変更内容:
|
|
5
|
+
-- - tier_recent_outcomes.session_id TEXT(既存行は NULL)
|
|
6
|
+
-- - idx_tier_recent_session ON tier_recent_outcomes(session_id)
|
|
7
|
+
-- - tier_bandit.total_cost_usd REAL NOT NULL DEFAULT 0.0 (v2.23.0 用確保のみ)
|
|
8
|
+
-- - tier_bandit.cost_samples INTEGER NOT NULL DEFAULT 0 (v2.23.0 用確保のみ)
|
|
9
|
+
--
|
|
10
|
+
-- 後方互換: ADD COLUMN DEFAULT により既存行は NULL / 0.0 / 0 で埋まる。
|
|
11
|
+
|
|
12
|
+
BEGIN;
|
|
13
|
+
|
|
14
|
+
ALTER TABLE tier_recent_outcomes ADD COLUMN session_id TEXT;
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_tier_recent_session
|
|
17
|
+
ON tier_recent_outcomes(session_id);
|
|
18
|
+
|
|
19
|
+
ALTER TABLE tier_bandit ADD COLUMN total_cost_usd REAL NOT NULL DEFAULT 0.0;
|
|
20
|
+
|
|
21
|
+
ALTER TABLE tier_bandit ADD COLUMN cost_samples INTEGER NOT NULL DEFAULT 0;
|
|
22
|
+
|
|
23
|
+
INSERT OR IGNORE INTO schema_migrations (version) VALUES ('003');
|
|
24
|
+
|
|
25
|
+
COMMIT;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- resolve_tier(model) -> str | None
|
|
11
11
|
- compute_cost_usd(...) -> tuple[float, bool]
|
|
12
12
|
- known_models() -> tuple[str, ...]
|
|
13
|
+
- tier_reference_cost(tier) -> float
|
|
13
14
|
|
|
14
15
|
設計判断(plan-report T1 §2):
|
|
15
16
|
Opus は世代で単価が約 3 倍異なる(4.1/4 = $15 系、4.5/4.6/4.7 = $5 系)ため、
|
|
@@ -148,6 +149,46 @@ def compute_cost_usd(
|
|
|
148
149
|
return (cost, True)
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# tier → 参照単価マッピング(v2.23.0 T1)
|
|
154
|
+
# tie-break 静的 fallback 用。tier 名から _PRICING キーへの対応表。
|
|
155
|
+
# 注意: select_tier.py の TIERS と同期必須。新 tier を TIERS に追加する場合は
|
|
156
|
+
# 本 dict も更新すること(未更新だと tier_reference_cost が未知 tier に 0.0 を返し、
|
|
157
|
+
# cost_map に混入して min-max 正規化で最安誤判定につながる)。
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
_TIER_REFERENCE_KEY: dict[str, str] = {
|
|
160
|
+
"haiku": "haiku-4-5",
|
|
161
|
+
"sonnet": "sonnet-4-5",
|
|
162
|
+
"opus": "opus-4-5",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def tier_reference_cost(tier: str) -> float:
|
|
167
|
+
"""tier 名から静的参照単価(input + output 単価和、USD/MTok)を返す。
|
|
168
|
+
|
|
169
|
+
tie-break の static fallback 用。実測 avg_cost が利用可能な場合は
|
|
170
|
+
実測値を優先し、本関数はデータ不足/欠損 tier の補完にのみ使用すること。
|
|
171
|
+
|
|
172
|
+
min-max 正規化で使用するため、絶対値より haiku < sonnet < opus の
|
|
173
|
+
順位が重要(_PRICING の現行世代単価で単調性が保証される)。
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
tier: "haiku" / "sonnet" / "opus" のいずれか。
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
_PRICING[_TIER_REFERENCE_KEY[tier]] の input + output 単価和(USD/MTok)。
|
|
180
|
+
未知 tier(_TIER_REFERENCE_KEY に存在しないキー)は 0.0 を返す。
|
|
181
|
+
"""
|
|
182
|
+
key = _TIER_REFERENCE_KEY.get(tier)
|
|
183
|
+
if key is None:
|
|
184
|
+
return 0.0
|
|
185
|
+
pricing = _PRICING.get(key)
|
|
186
|
+
if pricing is None:
|
|
187
|
+
return 0.0
|
|
188
|
+
inp_price, out_price, _cache_write, _cache_read = pricing
|
|
189
|
+
return inp_price + out_price
|
|
190
|
+
|
|
191
|
+
|
|
151
192
|
def known_models() -> tuple[str, ...]:
|
|
152
193
|
"""単価表のキー一覧を返す(テスト・デバッグ用)。
|
|
153
194
|
|