claude-code-conductor 2.45.0__tar.gz → 2.47.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.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/select_tier.py +33 -19
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/session_stop.py +7 -0
- claude_code_conductor-2.47.0/.claude/hooks/tier_autoapply.py +385 -0
- claude_code_conductor-2.47.0/.claude/hooks/tier_gap_check.py +346 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/settings.json +5 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/SKILL.md +80 -23
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/scripts/record_agent_outcome.py +161 -18
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +33 -5
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/parallel-agents/SKILL.md +9 -9
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/CHANGELOG.md +38 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/PKG-INFO +2 -1
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/README.md +1 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/__init__.py +1 -1
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/_db_params.py +8 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/_terminal.py +7 -2
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli.py +2 -0
- claude_code_conductor-2.47.0/src/c3/cli_metrics.py +351 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/db.py +673 -16
- claude_code_conductor-2.47.0/src/c3/migrations/006_review_decisions_severity.sql +17 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_record_agent_outcome.py +414 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_record_review_decision.py +171 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_review_hint_inject.py +63 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_select_tier.py +93 -56
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_session_stop.py +47 -0
- claude_code_conductor-2.47.0/tests/hooks/test_settings_tier_autoapply_registration.py +154 -0
- claude_code_conductor-2.47.0/tests/hooks/test_tier_autoapply.py +717 -0
- claude_code_conductor-2.47.0/tests/hooks/test_tier_gap_check.py +939 -0
- claude_code_conductor-2.47.0/tests/hooks/test_tier_security_fixes.py +735 -0
- claude_code_conductor-2.47.0/tests/test_cli_metrics.py +1198 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_db.py +1296 -2
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_migrate.py +116 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/CLAUDE.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/architect.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/code-reviewer.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/design-critic.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/developer.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/doc-writer.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/interviewer.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/planner.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/project-setup.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/security-reviewer.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/systematic-debugger.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/tester.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/wt_developer.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/wt_systematic-debugger.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/wt_tester.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/breaking-changes.txt +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/deletions.txt +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/config-policy.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/parallel-agents-setup.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/platform-adapters.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/settings.json.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/_hook_utils.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/check_agent_invocation.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/consolidate_memory.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/permission_handler.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/permission_handler_toast.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/planner_check.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/post_tool.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/pre_compact.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/pre_tool.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/recall_autorebuild.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/recall_inject.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/restore_session.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/session_start.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/session_utils.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/statusline.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/stop.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/worktree_guard.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/memory/.gitkeep +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/permission_rules.json +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/rules/promoted/index.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/brainstorm/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/codex-review/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/design-critic-rubric.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/design-rubric.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/interview-rubric.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/develop/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/doc/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/extract-lib/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/init-session/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/init-session/scripts/session_guard.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/mcp-config/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/pattern-status/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/recall/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/review-phase/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/reference.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/start/SKILL.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/state/.gitkeep +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.gitignore +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/ARCHITECTURE.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSE +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/fastembed-LICENSE +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/fastembed-NOTICE +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/numpy-LICENSE +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/onnxruntime-LICENSE +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/hatch_build.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/pyproject.toml +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/__main__.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/_excludes.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/adapters.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_ask.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_doctor.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_init.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_list.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_plan.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_recall.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_tier.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_update.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/embedding.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/gitutil.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/mcp_server.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrate.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/001_initial.sql +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/002_agent_cost_runs.sql +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/003_tier_cost.sql +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/004_agent_outcomes.sql +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/005_drop_agent_tier_bandit.sql +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/README.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/__init__.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/paths.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/plan_validator.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/platforms.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/pricing.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/question.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/recall_chunker.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/recall_index.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/usage_ingester.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/__init__.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/_pre_compact_helpers.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/conftest.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/README.md +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/mainline.jsonl +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/__init__.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_check_agent_invocation.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_consolidate_memory.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_hook_utils.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_permission_handler.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_permission_handler_toast.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_planner_check.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_planner_check_dev.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_post_tool.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_pre_tool.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_recall_autorebuild.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_recall_inject.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_restore_session.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_select_tier_escalation.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_session_start.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_session_utils.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_similarity_boost.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_statusline.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_statusline_template_sync.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_sync_check.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_template_guard.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/__init__.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/_skill_helpers.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_init_session_no_task_type.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_planner_lightweight.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_recall_skill.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_session_guard.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_setup_templates.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_start_skill_new_flow.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_adapters.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_check_deletions.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_ask.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_doctor.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_entry.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_init.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_list.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_plan.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_recall.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_tier.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_update_breaking_changes.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_update_deletions.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_docstring_consistency.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_embedding.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_excludes.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_extract_breaking_changes.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_gitutil.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_mcp_server_elicit.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_paths.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_plan_validator.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_pre_compact.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_precompact_additional.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_precompact_toctou_fixes.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_pricing.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_recall_chunker.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_recall_index.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_references_migration.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_session_utils_additional.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_skill_no_builtin_conflict.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_statusline.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_stop_additional.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_stop_hook.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_stop_precompact_fixes.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_sync_template_stop.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_template_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_usage_ingester.py +0 -0
- {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_worktree_guard.py +0 -0
|
@@ -5,18 +5,25 @@ tier-routing MVP: タスクの複雑度を簡易ヒューリスティックで
|
|
|
5
5
|
(または学習データ収集期は uniform random)で推奨 Tier を決定して
|
|
6
6
|
``additionalContext`` で親 Claude に伝える。
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
スコープ(ユーザー承認済み・tier-routing 自動適用フェーズ3で機械適用へ移行):
|
|
9
9
|
- **やる**: 推奨 Tier を提示し、developer / wt_developer の Agent 起動時に
|
|
10
|
-
`model:`
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
PreToolUse hook(tier_autoapply.py)が `model:` へ機械適用する
|
|
11
|
+
(updatedInput での自動書き換え・フェーズ3実装済み)。select_tier 自身は
|
|
12
|
+
推奨の提示と tier_selection.json への記録のみを担い、model の注入は
|
|
13
|
+
tier_autoapply.py が行う。明示指定された `model:` は tier_autoapply が
|
|
14
|
+
尊重して上書きしない。agent の `model:` フロントマター自体は変更しない
|
|
15
|
+
- **やらない**: developer / wt_developer 以外の role への適用、
|
|
16
|
+
フォールバック(Haiku 失敗→Sonnet 自動昇格)
|
|
17
|
+
|
|
18
|
+
学習データ(帰属機械化・フェーズ3):
|
|
17
19
|
- 結果は ``.claude/state/tier_selection.json`` に直近 1 件のみ書き込む
|
|
20
|
+
- Agent 起動時に ``tier_autoapply.py`` が実適用 model を
|
|
21
|
+
``.claude/state/tier_autoapply.jsonl`` へ記録する(適用者=記録 SSOT)
|
|
18
22
|
- dev-workflow の各フェーズ承認ゲート(並列タスク単位を含む)で
|
|
19
|
-
``record_agent_outcome.py``
|
|
23
|
+
``record_agent_outcome.py`` が applied-state(tier_autoapply.jsonl)を
|
|
24
|
+
session_id 一致で読んで実適用 tier を機械解決し(LLM 申告を介さない帰属機械化)、
|
|
25
|
+
``agent_outcomes`` イベントログに記録する。tier_selection.json はフォールバック
|
|
26
|
+
互換のため優先3 として温存する。
|
|
20
27
|
bandit params(α/β)は読み取り時に ``read_agent_tier_params`` が導出集計で計算する
|
|
21
28
|
|
|
22
29
|
入力 / 出力:
|
|
@@ -95,6 +102,11 @@ SIMILARITY_WEAK_THRESHOLD = 0.6 # この値以上で信頼度補強のみ
|
|
|
95
102
|
# 検出した値を *** に置換してから保存することで二次漏洩を防ぐ。
|
|
96
103
|
# NOTE: ここで値をマスクすることで類似度推定の精度も若干下がる可能性があるが、
|
|
97
104
|
# セキュリティ優先として許容する(設計書に記載なし: SR-V-001 対応と判断)。
|
|
105
|
+
# [CR-NEW1] _mask_secrets / _MASK_PATTERNS は以下の3ファイルに同型複製が存在した(パターン本体はバイト一致)。
|
|
106
|
+
# 語彙・パターンを変更する際は3ファイル全てを同期する(共通モジュール化は本サイクル未実施):
|
|
107
|
+
# - .claude/hooks/select_tier.py
|
|
108
|
+
# - .claude/hooks/tier_autoapply.py
|
|
109
|
+
# - .claude/skills/dev-workflow/scripts/record_agent_outcome.py
|
|
98
110
|
_MASK_PATTERNS: list[re.Pattern[str]] = [
|
|
99
111
|
re.compile(r'(password=)\S+', re.IGNORECASE),
|
|
100
112
|
re.compile(r'(api[_-]?key=)\S+', re.IGNORECASE),
|
|
@@ -107,11 +119,10 @@ _MASK_PATTERNS: list[re.Pattern[str]] = [
|
|
|
107
119
|
|
|
108
120
|
|
|
109
121
|
def _mask_secrets(text: str) -> str:
|
|
110
|
-
"""秘密情報パターンにマッチする値部分を ***
|
|
122
|
+
"""秘密情報パターンにマッチする値部分を *** に置換して返した。
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
PEM ブロックは開始タグ + *** + 終了タグ に置換する。
|
|
124
|
+
3ファイル同型複製(複製先は _MASK_PATTERNS 上の [CR-NEW1] コメント参照)。キー名や
|
|
125
|
+
プレフィックスは残し値のみ置換する。PEM は開始タグ + *** + 終了タグ に置換する。
|
|
115
126
|
"""
|
|
116
127
|
result = text
|
|
117
128
|
for pattern in _MASK_PATTERNS:
|
|
@@ -238,9 +249,12 @@ def _prompt_prefix_and_hash(prompt: str) -> tuple[str, str]:
|
|
|
238
249
|
|
|
239
250
|
Phase 2-C: prefix 200 文字 + SHA256 先頭 16 文字(プライバシー対策)。
|
|
240
251
|
SR-V-001: prefix に含まれる秘密情報パターンは *** にマスクしてから保存する。
|
|
252
|
+
SR-K-003: 全文をマスクしてから 200 字へ切り詰めた(マスク→切り詰め順)。
|
|
253
|
+
切り詰め→マスク順だと END タグが 200 字境界より後方にある PEM 等で
|
|
254
|
+
マスクがすり抜け鍵本体が平文残存したため是正した。
|
|
241
255
|
hash はマスク前の原文から計算する(一意性を保つため)。
|
|
242
256
|
"""
|
|
243
|
-
prefix = _mask_secrets(prompt[:_PROMPT_PREFIX_MAX]
|
|
257
|
+
prefix = _mask_secrets(prompt)[:_PROMPT_PREFIX_MAX]
|
|
244
258
|
h = hashlib.sha256(prompt.encode("utf-8", errors="replace")).hexdigest()[:16]
|
|
245
259
|
return prefix, h
|
|
246
260
|
|
|
@@ -603,11 +617,11 @@ def build_additional_context(
|
|
|
603
617
|
|
|
604
618
|
return (
|
|
605
619
|
f"[tier-routing 推奨] 複雑度: {complexity} / 推奨 Tier: {tier}({confidence}・developer 基準)。"
|
|
606
|
-
f"
|
|
607
|
-
f" model:
|
|
608
|
-
f"
|
|
609
|
-
f"
|
|
610
|
-
f"
|
|
620
|
+
f" developer / wt_developer を Agent ツールで起動する際は、PreToolUse hook"
|
|
621
|
+
f"(tier_autoapply)がこの推奨 Tier を model: に自動適用します"
|
|
622
|
+
f"(親 Claude が model: を転記する必要はありません・学習データ収集中の期間も含め常に適用)。"
|
|
623
|
+
f" 推奨と異なる Tier を使いたい場合のみ、Agent 呼び出し時に model: を明示指定してください"
|
|
624
|
+
f"(明示指定は尊重され hook に上書きされません。fork は model 上書き不可のため対象外)。"
|
|
611
625
|
f" 対象は developer / wt_developer のみ。tester 等の他 role、および親 Claude"
|
|
612
626
|
f" ペルソナで動かす role(interviewer / architect / planner)は"
|
|
613
627
|
f" tier レバーが無いため対象外で従来どおりとする。{suffix}"
|
|
@@ -106,6 +106,13 @@ def main() -> int:
|
|
|
106
106
|
except Exception as e:
|
|
107
107
|
print(f"[session_stop:usage_ingester] failed: {type(e).__name__}", file=sys.stderr)
|
|
108
108
|
|
|
109
|
+
# Phase 4: tier_gap_check — 起動ログ vs outcome 記録の欠落検針(警告のみ・副作用なし)
|
|
110
|
+
try:
|
|
111
|
+
gap_module = _load_module("tier_gap_check")
|
|
112
|
+
gap_module.run(payload)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"[session_stop:tier_gap_check] failed: {type(e).__name__}", file=sys.stderr)
|
|
115
|
+
|
|
109
116
|
return 0
|
|
110
117
|
|
|
111
118
|
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse Agent hook: tier-routing 機械適用(フェーズ3・T1)。
|
|
3
|
+
|
|
4
|
+
architecture-report-20260707-065043.md §3 の確定仕様を実装した本 hook は、
|
|
5
|
+
`matcher: Agent` の 2 本目として `check_agent_invocation.py` と同居する。
|
|
6
|
+
両 hook の作用 role は集合として排他(本 hook は LAUNCH_LOG_ROLES のみ・
|
|
7
|
+
check_agent は reviewer 系のみ)のため実行順序に依存しない(§1)。
|
|
8
|
+
|
|
9
|
+
## 動作(§3-2 ロール分岐表)
|
|
10
|
+
|
|
11
|
+
- `APPLY_ROLES = {developer, wt_developer}` かつ `model` 無指定かつ
|
|
12
|
+
tier_selection.json から有効 tier を解決できたとき、`updatedInput` で
|
|
13
|
+
`model:{推奨tier}` を注入した(source=injected・permissionDecision は
|
|
14
|
+
T0 実測により省略形を正とした)。
|
|
15
|
+
- 明示 `model` 指定時は素通り(source=explicit・明示尊重)。
|
|
16
|
+
- tier_selection 不在/破損/非文字列 tier のときは注入せず素通り
|
|
17
|
+
(source=frontmatter-default・frontmatter の sonnet が実効するため)。
|
|
18
|
+
- `LAUNCH_LOG_ROLES = {developer, wt_developer, tester, wt_tester}` は
|
|
19
|
+
注入有無に関わらず applied-state(tier_autoapply.jsonl)へ 1 行追記した。
|
|
20
|
+
reviewer 系・その他 role は記録も注入もしない(exit 0 素通り)。
|
|
21
|
+
|
|
22
|
+
## 安全弁
|
|
23
|
+
|
|
24
|
+
- kill-switch: `C3_TIER_AUTOAPPLY_DISABLE=1` で注入も記録も行わず exit 0
|
|
25
|
+
(旧来のソフト適用のみ動作へ完全復帰)。
|
|
26
|
+
- fail-safe: 不正 JSON・非 Agent・非 dict tool_input・想定外例外は全て握って
|
|
27
|
+
exit 0(`check_agent_invocation.py` の流儀を踏襲)。
|
|
28
|
+
- ts は `agent_outcomes.ts`(record 経由・`src/c3/db.py:1046`)と同一プロファイル
|
|
29
|
+
`datetime.now(timezone.utc).isoformat(timespec="seconds")` で書いた
|
|
30
|
+
(§0-4(d)・§5-2 項3 の跨りソース `ts_floor` 辞書順比較の成立条件)。
|
|
31
|
+
|
|
32
|
+
## パス解決(§3-7・DC-AS-003)
|
|
33
|
+
|
|
34
|
+
`.claude/hooks/` は 1 階層遡りで `_CLAUDE_DIR` を求め、`record_agent_outcome.py`
|
|
35
|
+
(3 階層遡り)と同一の `.claude/state/tier_autoapply.jsonl` に解決した。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import collections
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import re
|
|
44
|
+
import sys
|
|
45
|
+
from datetime import datetime, timezone
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
sys.stdin.reconfigure(encoding="utf-8")
|
|
49
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
50
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
51
|
+
except AttributeError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
# 並行 append の排他用 OS ファイルロック(プラットフォーム別)。Windows の
|
|
55
|
+
# append モードはプロセス間で atomic でなく、複数 subagent 同時起動時に行が
|
|
56
|
+
# 欠落しうるため、専用ロックファイルの OS ロックで直列化した(§3-4)。
|
|
57
|
+
try:
|
|
58
|
+
import msvcrt # type: ignore[import-not-found]
|
|
59
|
+
except ImportError:
|
|
60
|
+
msvcrt = None # type: ignore[assignment]
|
|
61
|
+
try:
|
|
62
|
+
import fcntl # type: ignore[import-not-found]
|
|
63
|
+
except ImportError:
|
|
64
|
+
fcntl = None # type: ignore[assignment]
|
|
65
|
+
|
|
66
|
+
# このファイルは .claude/hooks/ に置かれている前提。1 階層遡って .claude/ を得た:
|
|
67
|
+
# hooks/ → .claude/
|
|
68
|
+
_HOOK_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
69
|
+
_CLAUDE_DIR = os.path.dirname(_HOOK_DIR)
|
|
70
|
+
# 1 階層遡りで `.claude` に到達することを実行時に検証した(record_agent_outcome.py
|
|
71
|
+
# の _CLAUDE_DIR 検証と同一思想。誤配置のサイレント破綻を防ぐ・開発時チェック)。
|
|
72
|
+
if not (_CLAUDE_DIR.endswith(os.sep + ".claude") or _CLAUDE_DIR.endswith("/.claude")):
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
f"_CLAUDE_DIR resolution broke: expected to end with '.claude' but got "
|
|
75
|
+
f"{_CLAUDE_DIR!r}. Check that this file is at .claude/hooks/."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
TIER_SELECTION_PATH = os.path.join(_CLAUDE_DIR, "state", "tier_selection.json")
|
|
79
|
+
APPLIED_STATE_PATH = os.path.join(_CLAUDE_DIR, "state", "tier_autoapply.jsonl")
|
|
80
|
+
|
|
81
|
+
# §3-2 ロール分岐表の定数。
|
|
82
|
+
APPLY_ROLES = frozenset({"developer", "wt_developer"})
|
|
83
|
+
LAUNCH_LOG_ROLES = frozenset({"developer", "wt_developer", "tester", "wt_tester"})
|
|
84
|
+
# wt_* → 無印への正規化(record 側 role 語彙・agent_outcomes.role と一致・§0-4)。
|
|
85
|
+
_ROLE_NORMALIZE = {"wt_developer": "developer", "wt_tester": "tester"}
|
|
86
|
+
|
|
87
|
+
_VALID_TIERS = ("haiku", "sonnet", "opus")
|
|
88
|
+
|
|
89
|
+
KILL_SWITCH_ENV = "C3_TIER_AUTOAPPLY_DISABLE"
|
|
90
|
+
|
|
91
|
+
# DoS 防御の stdin 読み取り上限(check_agent_invocation.py と同一)。
|
|
92
|
+
_STDIN_READ_LIMIT_BYTES = 1 * 1024 * 1024 # 1 MiB
|
|
93
|
+
|
|
94
|
+
# applied-state のローテーション閾値(§3-4)。
|
|
95
|
+
_MAX_JSONL_BYTES = 1 * 1024 * 1024 # 1 MB
|
|
96
|
+
_ROTATE_TAIL_LINES = 500
|
|
97
|
+
|
|
98
|
+
_PROMPT_PREFIX_MAX_CHARS = 200
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _load_pricing_module():
|
|
102
|
+
"""c3.pricing モジュールを返した(import 失敗時は None)。"""
|
|
103
|
+
try:
|
|
104
|
+
from c3 import pricing as c3_pricing # type: ignore[import-not-found]
|
|
105
|
+
|
|
106
|
+
return c3_pricing
|
|
107
|
+
except ImportError:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# item3(SR-K-003・Low): prompt_prefix の秘密情報マスクパターン。hooks は import
|
|
112
|
+
# 非依存方針のため他モジュールを import せず同型を複製した。
|
|
113
|
+
# キー名やプレフィックスは残し、値のみを *** に置換する。PEM ブロックは
|
|
114
|
+
# 開始タグ + *** + 終了タグ に置換する。
|
|
115
|
+
# [CR-NEW1] _mask_secrets / _MASK_PATTERNS は以下の3ファイルに同型複製が存在した(パターン本体はバイト一致)。
|
|
116
|
+
# 語彙・パターンを変更する際は3ファイル全てを同期する(共通モジュール化は本サイクル未実施):
|
|
117
|
+
# - .claude/hooks/select_tier.py
|
|
118
|
+
# - .claude/hooks/tier_autoapply.py
|
|
119
|
+
# - .claude/skills/dev-workflow/scripts/record_agent_outcome.py
|
|
120
|
+
_MASK_PATTERNS: list[re.Pattern[str]] = [
|
|
121
|
+
re.compile(r'(password=)\S+', re.IGNORECASE),
|
|
122
|
+
re.compile(r'(api[_-]?key=)\S+', re.IGNORECASE),
|
|
123
|
+
re.compile(r'(Bearer\s+)[\w\-\.]+', re.IGNORECASE),
|
|
124
|
+
re.compile(r'(\btoken=)\S+', re.IGNORECASE),
|
|
125
|
+
re.compile(r'(\bsecret=)\S+', re.IGNORECASE),
|
|
126
|
+
re.compile(r'(aws_secret_access_key=)\S+', re.IGNORECASE),
|
|
127
|
+
re.compile(r'(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)'),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _mask_secrets(text: str) -> str:
|
|
132
|
+
"""秘密情報パターンにマッチする値部分を *** に置換して返した。
|
|
133
|
+
|
|
134
|
+
3ファイル同型複製(複製先は _MASK_PATTERNS 上の [CR-NEW1] コメント参照)。キー名や
|
|
135
|
+
プレフィックスは残し値のみ置換する。PEM は開始タグ + *** + 終了タグ に置換する。
|
|
136
|
+
"""
|
|
137
|
+
result = text
|
|
138
|
+
for pattern in _MASK_PATTERNS:
|
|
139
|
+
# group(2) があれば PEM ブロック(BEGIN...END)、なければプレフィックス系。
|
|
140
|
+
result = pattern.sub(
|
|
141
|
+
lambda m: m.group(1) + "***" + (m.group(2) if m.lastindex and m.lastindex >= 2 else ""),
|
|
142
|
+
result,
|
|
143
|
+
)
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _clean_prompt_prefix(prompt: object) -> str:
|
|
148
|
+
"""prompt の秘密情報をマスクし、先頭200字を制御文字除去のうえ返した(§3-3)。
|
|
149
|
+
|
|
150
|
+
item3(SR-K-003): 制御文字除去・切り詰めより前に _mask_secrets を適用する。
|
|
151
|
+
切り詰め境界をまたぐ秘密や、制御文字混入でパターン境界が崩れる前にマスクを
|
|
152
|
+
確定させるため(record_agent_outcome.py の prompt-history 経路と同等の軽減)。
|
|
153
|
+
"""
|
|
154
|
+
if not isinstance(prompt, str):
|
|
155
|
+
return ""
|
|
156
|
+
prompt = _mask_secrets(prompt)
|
|
157
|
+
out = []
|
|
158
|
+
for ch in prompt:
|
|
159
|
+
code = ord(ch)
|
|
160
|
+
if code < 0x20: # C0 制御文字(\t \r \n 含む)
|
|
161
|
+
continue
|
|
162
|
+
if 0x7F <= code <= 0x9F: # DEL + C1 制御文字
|
|
163
|
+
continue
|
|
164
|
+
if code in (0x2028, 0x2029): # LINE / PARAGRAPH SEPARATOR
|
|
165
|
+
continue
|
|
166
|
+
out.append(ch)
|
|
167
|
+
return "".join(out)[:_PROMPT_PREFIX_MAX_CHARS]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _read_selection() -> dict | None:
|
|
171
|
+
"""tier_selection.json を読んだ(無い/壊れていれば None)。"""
|
|
172
|
+
if not os.path.isfile(TIER_SELECTION_PATH):
|
|
173
|
+
return None
|
|
174
|
+
try:
|
|
175
|
+
with open(TIER_SELECTION_PATH, "r", encoding="utf-8") as f:
|
|
176
|
+
data = json.load(f)
|
|
177
|
+
except (OSError, json.JSONDecodeError):
|
|
178
|
+
return None
|
|
179
|
+
if not isinstance(data, dict):
|
|
180
|
+
return None
|
|
181
|
+
return data
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _resolve_selection_tier(c3_pricing) -> str | None:
|
|
185
|
+
"""tier_selection.json の tier(無ければ suggested_model)を正規化 tier に解決した。
|
|
186
|
+
|
|
187
|
+
非文字列・破損・不在・正規化不能は None を返した(→ 注入せず frontmatter-default)。
|
|
188
|
+
"""
|
|
189
|
+
if c3_pricing is None:
|
|
190
|
+
return None
|
|
191
|
+
selection = _read_selection()
|
|
192
|
+
if selection is None:
|
|
193
|
+
return None
|
|
194
|
+
raw = selection.get("tier") or selection.get("suggested_model")
|
|
195
|
+
if not isinstance(raw, str):
|
|
196
|
+
return None
|
|
197
|
+
resolved = c3_pricing.resolve_tier(raw)
|
|
198
|
+
if resolved in _VALID_TIERS:
|
|
199
|
+
return resolved
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _rotate_if_needed(path: str) -> None:
|
|
204
|
+
"""applied-state が 1MB を超えていたら末尾500行へ切り詰めた(§3-4・失敗時は無視)。"""
|
|
205
|
+
try:
|
|
206
|
+
size = os.path.getsize(path)
|
|
207
|
+
except OSError:
|
|
208
|
+
return
|
|
209
|
+
if size <= _MAX_JSONL_BYTES:
|
|
210
|
+
return
|
|
211
|
+
try:
|
|
212
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
213
|
+
tail = list(collections.deque(f, maxlen=_ROTATE_TAIL_LINES))
|
|
214
|
+
tmp_path = path + ".tmp"
|
|
215
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
216
|
+
f.writelines(tail)
|
|
217
|
+
os.replace(tmp_path, path)
|
|
218
|
+
except OSError:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _os_lock(lock_f) -> None:
|
|
223
|
+
"""ロックファイルのバイト0に排他ロックを取得した(取得不能時は例外を送出)。"""
|
|
224
|
+
if msvcrt is not None:
|
|
225
|
+
lock_f.seek(0)
|
|
226
|
+
msvcrt.locking(lock_f.fileno(), msvcrt.LK_LOCK, 1)
|
|
227
|
+
elif fcntl is not None:
|
|
228
|
+
fcntl.flock(lock_f.fileno(), fcntl.LOCK_EX)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _os_unlock(lock_f) -> None:
|
|
232
|
+
"""_os_lock で取得したロックを解放した(失敗時は無視)。"""
|
|
233
|
+
try:
|
|
234
|
+
if msvcrt is not None:
|
|
235
|
+
lock_f.seek(0)
|
|
236
|
+
msvcrt.locking(lock_f.fileno(), msvcrt.LK_UNLCK, 1)
|
|
237
|
+
elif fcntl is not None:
|
|
238
|
+
fcntl.flock(lock_f.fileno(), fcntl.LOCK_UN)
|
|
239
|
+
except OSError:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _append_applied_state(row: dict) -> None:
|
|
244
|
+
"""applied-state(jsonl)へ 1 行を単一 write で追記した(§3-4・追記前にローテーション)。
|
|
245
|
+
|
|
246
|
+
専用ロックファイル(`<jsonl>.lock`)の OS ロックで並行 append を直列化した。
|
|
247
|
+
ロックはハンドルクローズ/プロセス終了時に OS が自動解放するため、
|
|
248
|
+
ロックファイルが残っても後続の追記をブロックしない(stale-lock デッドロック回避)。
|
|
249
|
+
|
|
250
|
+
ロックファイル(`<jsonl>.lock`)の永続化について(CR F-6):
|
|
251
|
+
このファイルは削除せず `.claude/state/*` の gitignore exclude パターンで
|
|
252
|
+
配布対象外にしている(_rotate_if_needed 同様の設計)。stale-lock 回避のため
|
|
253
|
+
OS ハンドル解放に委ね、明示的な削除を行わない。
|
|
254
|
+
"""
|
|
255
|
+
line = json.dumps(row, ensure_ascii=False)
|
|
256
|
+
lock_path = APPLIED_STATE_PATH + ".lock"
|
|
257
|
+
# item2(SR-V-002・Low): jsonl 本体・ロックファイルのいずれかが symlink の場合は
|
|
258
|
+
# 追記全体を沈黙 skip(リンク先への誘導書き込みを防ぐ・fail-safe)。open で
|
|
259
|
+
# follow する前に検証する。Windows は O_NOFOLLOW 非対応のため islink で分岐する
|
|
260
|
+
# (POSIX 慣行に合わせた可搬な検証)。tier_gap_check.py 読み取り側と同型。
|
|
261
|
+
# [CR-NEW3] islink 検証→open 間の TOCTOU レースは構造的に残存するが既知・脅威モデル外
|
|
262
|
+
# (Windows O_NOFOLLOW 非対応による設計選択。stale-lock TOCTOU と同種の回避不能パターン)。
|
|
263
|
+
if os.path.islink(APPLIED_STATE_PATH) or os.path.islink(lock_path):
|
|
264
|
+
return
|
|
265
|
+
try:
|
|
266
|
+
os.makedirs(os.path.dirname(APPLIED_STATE_PATH), exist_ok=True)
|
|
267
|
+
except OSError as exc:
|
|
268
|
+
print(f"[tier_autoapply] mkdir failed: {type(exc).__name__}", file=sys.stderr)
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
lock_f = None
|
|
272
|
+
try:
|
|
273
|
+
lock_f = open(lock_path, "a+", encoding="utf-8")
|
|
274
|
+
except OSError:
|
|
275
|
+
lock_f = None
|
|
276
|
+
|
|
277
|
+
locked = False
|
|
278
|
+
try:
|
|
279
|
+
if lock_f is not None:
|
|
280
|
+
try:
|
|
281
|
+
_os_lock(lock_f)
|
|
282
|
+
locked = True
|
|
283
|
+
except OSError:
|
|
284
|
+
locked = False # ロック不能でもベストエフォートで追記する。
|
|
285
|
+
_rotate_if_needed(APPLIED_STATE_PATH)
|
|
286
|
+
with open(APPLIED_STATE_PATH, "a", encoding="utf-8") as f:
|
|
287
|
+
f.write(line + "\n")
|
|
288
|
+
except OSError as exc:
|
|
289
|
+
print(f"[tier_autoapply] append failed: {type(exc).__name__}", file=sys.stderr)
|
|
290
|
+
finally:
|
|
291
|
+
if lock_f is not None:
|
|
292
|
+
if locked:
|
|
293
|
+
_os_unlock(lock_f)
|
|
294
|
+
lock_f.close()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main() -> None:
|
|
298
|
+
# kill-switch: 注入も記録も行わず旧来動作へ完全復帰した(§3-5)。
|
|
299
|
+
# 無駄な I/O(stdin 読み取り・JSON パース)を避けるため kill-switch は stdin 読み取り前に判定する(CR F-5)。
|
|
300
|
+
if os.environ.get(KILL_SWITCH_ENV) == "1":
|
|
301
|
+
sys.exit(0)
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
payload = json.loads(sys.stdin.read(_STDIN_READ_LIMIT_BYTES))
|
|
305
|
+
except (json.JSONDecodeError, ValueError):
|
|
306
|
+
sys.exit(0)
|
|
307
|
+
if not isinstance(payload, dict):
|
|
308
|
+
sys.exit(0)
|
|
309
|
+
|
|
310
|
+
if payload.get("tool_name") != "Agent":
|
|
311
|
+
sys.exit(0)
|
|
312
|
+
|
|
313
|
+
tool_input = payload.get("tool_input")
|
|
314
|
+
if not isinstance(tool_input, dict):
|
|
315
|
+
sys.exit(0)
|
|
316
|
+
|
|
317
|
+
subagent_type = tool_input.get("subagent_type", "")
|
|
318
|
+
if not isinstance(subagent_type, str):
|
|
319
|
+
subagent_type = ""
|
|
320
|
+
|
|
321
|
+
# LAUNCH_LOG_ROLES 以外(reviewer 系・その他)は記録も注入もせず素通りした。
|
|
322
|
+
if subagent_type not in LAUNCH_LOG_ROLES:
|
|
323
|
+
sys.exit(0)
|
|
324
|
+
|
|
325
|
+
role_recorded = _ROLE_NORMALIZE.get(subagent_type, subagent_type)
|
|
326
|
+
session_id = payload.get("session_id")
|
|
327
|
+
if not isinstance(session_id, str):
|
|
328
|
+
session_id = None
|
|
329
|
+
|
|
330
|
+
model_val = tool_input.get("model")
|
|
331
|
+
has_explicit_model = isinstance(model_val, str) and model_val.strip() != ""
|
|
332
|
+
|
|
333
|
+
inject = False
|
|
334
|
+
if has_explicit_model:
|
|
335
|
+
# 明示尊重: 注入せず素通りし explicit で記録した。
|
|
336
|
+
source = "explicit"
|
|
337
|
+
model_applied: str | None = model_val
|
|
338
|
+
elif subagent_type in APPLY_ROLES:
|
|
339
|
+
c3_pricing = _load_pricing_module()
|
|
340
|
+
tier = _resolve_selection_tier(c3_pricing)
|
|
341
|
+
if tier is not None:
|
|
342
|
+
inject = True
|
|
343
|
+
source = "injected"
|
|
344
|
+
model_applied = tier
|
|
345
|
+
else:
|
|
346
|
+
# tier_selection 不在/破損/非文字列: frontmatter の既定が実効するため記録のみ。
|
|
347
|
+
source = "frontmatter-default"
|
|
348
|
+
model_applied = None
|
|
349
|
+
else:
|
|
350
|
+
# tester/wt_tester: 注入対象外(記録のみ)。frontmatter の既定が実効した。
|
|
351
|
+
source = "frontmatter-default"
|
|
352
|
+
model_applied = None
|
|
353
|
+
|
|
354
|
+
row = {
|
|
355
|
+
"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
356
|
+
"session_id": session_id,
|
|
357
|
+
"subagent_type": subagent_type,
|
|
358
|
+
"role_recorded": role_recorded,
|
|
359
|
+
"model_applied": model_applied,
|
|
360
|
+
"source": source,
|
|
361
|
+
"prompt_prefix": _clean_prompt_prefix(tool_input.get("prompt")),
|
|
362
|
+
}
|
|
363
|
+
_append_applied_state(row)
|
|
364
|
+
|
|
365
|
+
if inject:
|
|
366
|
+
updated_input = {**tool_input, "model": model_applied}
|
|
367
|
+
output = {
|
|
368
|
+
"hookSpecificOutput": {
|
|
369
|
+
"hookEventName": "PreToolUse",
|
|
370
|
+
"updatedInput": updated_input,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
374
|
+
|
|
375
|
+
sys.exit(0)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
if __name__ == "__main__":
|
|
379
|
+
try:
|
|
380
|
+
main()
|
|
381
|
+
except SystemExit:
|
|
382
|
+
raise
|
|
383
|
+
except Exception as exc: # noqa: BLE001 — fail-safe: 想定外例外も握って exit 0。
|
|
384
|
+
print(f"[tier_autoapply] unexpected error: {type(exc).__name__}", file=sys.stderr)
|
|
385
|
+
sys.exit(0)
|