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.
Files changed (219) hide show
  1. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/select_tier.py +33 -19
  2. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/session_stop.py +7 -0
  3. claude_code_conductor-2.47.0/.claude/hooks/tier_autoapply.py +385 -0
  4. claude_code_conductor-2.47.0/.claude/hooks/tier_gap_check.py +346 -0
  5. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/settings.json +5 -0
  6. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/SKILL.md +80 -23
  7. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/scripts/record_agent_outcome.py +161 -18
  8. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +33 -5
  9. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/parallel-agents/SKILL.md +9 -9
  10. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/CHANGELOG.md +38 -0
  11. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/PKG-INFO +2 -1
  12. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/README.md +1 -0
  13. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/__init__.py +1 -1
  14. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/_db_params.py +8 -0
  15. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/_terminal.py +7 -2
  16. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli.py +2 -0
  17. claude_code_conductor-2.47.0/src/c3/cli_metrics.py +351 -0
  18. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/db.py +673 -16
  19. claude_code_conductor-2.47.0/src/c3/migrations/006_review_decisions_severity.sql +17 -0
  20. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_record_agent_outcome.py +414 -0
  21. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_record_review_decision.py +171 -0
  22. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_review_hint_inject.py +63 -0
  23. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_select_tier.py +93 -56
  24. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_session_stop.py +47 -0
  25. claude_code_conductor-2.47.0/tests/hooks/test_settings_tier_autoapply_registration.py +154 -0
  26. claude_code_conductor-2.47.0/tests/hooks/test_tier_autoapply.py +717 -0
  27. claude_code_conductor-2.47.0/tests/hooks/test_tier_gap_check.py +939 -0
  28. claude_code_conductor-2.47.0/tests/hooks/test_tier_security_fixes.py +735 -0
  29. claude_code_conductor-2.47.0/tests/test_cli_metrics.py +1198 -0
  30. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_db.py +1296 -2
  31. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_migrate.py +116 -0
  32. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/CLAUDE.md +0 -0
  33. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/architect.md +0 -0
  34. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/code-reviewer.md +0 -0
  35. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/design-critic.md +0 -0
  36. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/developer.md +0 -0
  37. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/doc-writer.md +0 -0
  38. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/interviewer.md +0 -0
  39. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/planner.md +0 -0
  40. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/project-setup.md +0 -0
  41. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/security-reviewer.md +0 -0
  42. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/systematic-debugger.md +0 -0
  43. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/tester.md +0 -0
  44. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/wt_developer.md +0 -0
  45. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/wt_systematic-debugger.md +0 -0
  46. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/agents/wt_tester.md +0 -0
  47. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/breaking-changes.txt +0 -0
  48. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/deletions.txt +0 -0
  49. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/config-policy.md +0 -0
  50. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/parallel-agents-setup.md +0 -0
  51. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/platform-adapters.md +0 -0
  52. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/docs/settings.json.md +0 -0
  53. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/_hook_utils.py +0 -0
  54. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/check_agent_invocation.py +0 -0
  55. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/consolidate_memory.py +0 -0
  56. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/permission_handler.py +0 -0
  57. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/permission_handler_toast.py +0 -0
  58. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/planner_check.py +0 -0
  59. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/post_tool.py +0 -0
  60. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/pre_compact.py +0 -0
  61. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/pre_tool.py +0 -0
  62. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/recall_autorebuild.py +0 -0
  63. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/recall_inject.py +0 -0
  64. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/restore_session.py +0 -0
  65. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/session_start.py +0 -0
  66. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/session_utils.py +0 -0
  67. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/statusline.py +0 -0
  68. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/stop.py +0 -0
  69. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/hooks/worktree_guard.py +0 -0
  70. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/memory/.gitkeep +0 -0
  71. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/permission_rules.json +0 -0
  72. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/rules/promoted/index.md +0 -0
  73. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/brainstorm/SKILL.md +0 -0
  74. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/codex-review/SKILL.md +0 -0
  75. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
  76. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/design-critic-rubric.md +0 -0
  77. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/design-rubric.md +0 -0
  78. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/interview-rubric.md +0 -0
  79. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
  80. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
  81. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
  82. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/develop/SKILL.md +0 -0
  83. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/doc/SKILL.md +0 -0
  84. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/extract-lib/SKILL.md +0 -0
  85. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/init-session/SKILL.md +0 -0
  86. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/init-session/scripts/session_guard.py +0 -0
  87. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/mcp-config/SKILL.md +0 -0
  88. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/pattern-status/SKILL.md +0 -0
  89. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
  90. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/recall/SKILL.md +0 -0
  91. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
  92. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
  93. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/review-phase/SKILL.md +0 -0
  94. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/SKILL.md +0 -0
  95. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/reference.md +0 -0
  96. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
  97. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
  98. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/skills/start/SKILL.md +0 -0
  99. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.claude/state/.gitkeep +0 -0
  100. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/.gitignore +0 -0
  101. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/ARCHITECTURE.md +0 -0
  102. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSE +0 -0
  103. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/fastembed-LICENSE +0 -0
  104. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/fastembed-NOTICE +0 -0
  105. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/numpy-LICENSE +0 -0
  106. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/onnxruntime-LICENSE +0 -0
  107. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
  108. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/hatch_build.py +0 -0
  109. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/pyproject.toml +0 -0
  110. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/__main__.py +0 -0
  111. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/_excludes.py +0 -0
  112. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/adapters.py +0 -0
  113. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_ask.py +0 -0
  114. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_doctor.py +0 -0
  115. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_init.py +0 -0
  116. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_list.py +0 -0
  117. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_plan.py +0 -0
  118. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_recall.py +0 -0
  119. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_tier.py +0 -0
  120. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/cli_update.py +0 -0
  121. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/embedding.py +0 -0
  122. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/gitutil.py +0 -0
  123. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/mcp_server.py +0 -0
  124. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrate.py +0 -0
  125. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/001_initial.sql +0 -0
  126. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/002_agent_cost_runs.sql +0 -0
  127. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/003_tier_cost.sql +0 -0
  128. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/004_agent_outcomes.sql +0 -0
  129. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/005_drop_agent_tier_bandit.sql +0 -0
  130. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/README.md +0 -0
  131. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/migrations/__init__.py +0 -0
  132. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/paths.py +0 -0
  133. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/plan_validator.py +0 -0
  134. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/platforms.py +0 -0
  135. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/pricing.py +0 -0
  136. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/question.py +0 -0
  137. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/recall_chunker.py +0 -0
  138. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/recall_index.py +0 -0
  139. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/src/c3/usage_ingester.py +0 -0
  140. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/__init__.py +0 -0
  141. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/_pre_compact_helpers.py +0 -0
  142. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/conftest.py +0 -0
  143. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/README.md +0 -0
  144. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/mainline.jsonl +0 -0
  145. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +0 -0
  146. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +0 -0
  147. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/__init__.py +0 -0
  148. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_check_agent_invocation.py +0 -0
  149. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_consolidate_memory.py +0 -0
  150. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_hook_utils.py +0 -0
  151. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_permission_handler.py +0 -0
  152. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_permission_handler_toast.py +0 -0
  153. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
  154. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_planner_check.py +0 -0
  155. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_planner_check_dev.py +0 -0
  156. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_post_tool.py +0 -0
  157. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_pre_tool.py +0 -0
  158. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_recall_autorebuild.py +0 -0
  159. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_recall_inject.py +0 -0
  160. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_restore_session.py +0 -0
  161. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_select_tier_escalation.py +0 -0
  162. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_session_start.py +0 -0
  163. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_session_utils.py +0 -0
  164. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
  165. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_similarity_boost.py +0 -0
  166. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_statusline.py +0 -0
  167. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_statusline_template_sync.py +0 -0
  168. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_sync_check.py +0 -0
  169. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/hooks/test_template_guard.py +0 -0
  170. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/__init__.py +0 -0
  171. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/_skill_helpers.py +0 -0
  172. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
  173. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_init_session_no_task_type.py +0 -0
  174. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_planner_lightweight.py +0 -0
  175. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_recall_skill.py +0 -0
  176. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
  177. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_session_guard.py +0 -0
  178. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_setup_templates.py +0 -0
  179. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
  180. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_start_skill_new_flow.py +0 -0
  181. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
  182. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_adapters.py +0 -0
  183. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_check_deletions.py +0 -0
  184. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_ask.py +0 -0
  185. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_doctor.py +0 -0
  186. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_entry.py +0 -0
  187. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_init.py +0 -0
  188. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_list.py +0 -0
  189. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_plan.py +0 -0
  190. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_recall.py +0 -0
  191. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_tier.py +0 -0
  192. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_update_breaking_changes.py +0 -0
  193. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_cli_update_deletions.py +0 -0
  194. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_docstring_consistency.py +0 -0
  195. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_embedding.py +0 -0
  196. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_excludes.py +0 -0
  197. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_extract_breaking_changes.py +0 -0
  198. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_gitutil.py +0 -0
  199. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_mcp_server_elicit.py +0 -0
  200. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_paths.py +0 -0
  201. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_plan_validator.py +0 -0
  202. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_pre_compact.py +0 -0
  203. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_pre_tool_hook.py +0 -0
  204. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_precompact_additional.py +0 -0
  205. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_precompact_toctou_fixes.py +0 -0
  206. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_pricing.py +0 -0
  207. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_recall_chunker.py +0 -0
  208. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_recall_index.py +0 -0
  209. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_references_migration.py +0 -0
  210. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_session_utils_additional.py +0 -0
  211. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_skill_no_builtin_conflict.py +0 -0
  212. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_statusline.py +0 -0
  213. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_stop_additional.py +0 -0
  214. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_stop_hook.py +0 -0
  215. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_stop_precompact_fixes.py +0 -0
  216. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_sync_template_stop.py +0 -0
  217. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_template_pre_tool_hook.py +0 -0
  218. {claude_code_conductor-2.45.0 → claude_code_conductor-2.47.0}/tests/test_usage_ingester.py +0 -0
  219. {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
- MVP スコープ(ユーザー承認済み・tier-routing 自動適用フェーズ2でソフト適用を追加):
8
+ スコープ(ユーザー承認済み・tier-routing 自動適用フェーズ3で機械適用へ移行):
9
9
  - **やる**: 推奨 Tier を提示し、developer / wt_developer の Agent 起動時に
10
- `model:` へ明示適用するよう親 Claude に指示する(ソフト適用・
11
- additionalContext 経由の行動指示であり機械強制ではない)。agent
12
- `model:` フロントマター自体は変更しない
13
- - **やらない**: PreToolUse hook 等による機械適用(updatedInput での自動書き換え・
14
- 次フェーズ)、developer 以外の role への適用、フォールバック(Haiku 失敗→Sonnet 自動昇格)
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`` が結果を ``agent_outcomes`` イベントログに記録し、
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" エージェント定義 frontmatter model がデフォルトですが、Agent 呼び出し時に"
607
- f" model: を明示指定すれば上書きできます(fork を除く)。"
608
- f" 指示: developer / wt_developer を Agent ツールで起動する際は"
609
- f" model: {tier} を明示指定してこの推奨 Tier を適用すること"
610
- f"(学習データ収集中の期間も含め常に適用する。fork は model 上書き不可のため対象外)。"
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)