claude-code-conductor 2.5.0__tar.gz → 2.6.1__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 (135) hide show
  1. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/consolidate_memory.py +25 -6
  2. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/permission_handler.py +28 -9
  3. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/post_tool.py +1 -0
  4. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/restore_session.py +16 -9
  5. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/review_hint_inject.py +10 -2
  6. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/select_tier.py +52 -15
  7. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_utils.py +1 -2
  8. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/stop.py +12 -2
  9. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/settings.json +1 -0
  10. claude_code_conductor-2.6.1/.claude/skills/codex-review/SKILL.md +211 -0
  11. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/CHANGELOG.md +76 -0
  12. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/PKG-INFO +1 -1
  13. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/__init__.py +1 -1
  14. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/adapters.py +6 -1
  15. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli_ask.py +15 -2
  16. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli_tier.py +6 -27
  17. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/db.py +55 -1
  18. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/mcp_server.py +9 -1
  19. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/question.py +6 -2
  20. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_consolidate_memory.py +168 -0
  21. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_permission_handler.py +66 -0
  22. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_planner_check.py +96 -0
  23. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_select_tier.py +96 -0
  24. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_cli_ask.py +69 -0
  25. claude_code_conductor-2.6.1/tests/test_mcp_server_elicit.py +113 -0
  26. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/CLAUDE.md +0 -0
  27. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/architect.md +0 -0
  28. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/code-reviewer.md +0 -0
  29. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/developer.md +0 -0
  30. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/doc-writer.md +0 -0
  31. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/interviewer.md +0 -0
  32. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/planner.md +0 -0
  33. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/project-setup.md +0 -0
  34. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/security-reviewer.md +0 -0
  35. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/systematic-debugger.md +0 -0
  36. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/tester.md +0 -0
  37. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_developer.md +0 -0
  38. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_systematic-debugger.md +0 -0
  39. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_tester.md +0 -0
  40. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/docs/platform-adapters.md +0 -0
  41. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/docs/settings.json.md +0 -0
  42. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/pre_compact.py +0 -0
  43. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/pre_tool.py +0 -0
  44. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/record_review_decision.py +0 -0
  45. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/record_tier_outcome.py +0 -0
  46. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/schema.sql +0 -0
  47. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_start.py +0 -0
  48. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_stop.py +0 -0
  49. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/statusline.py +0 -0
  50. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/subagent_log.py +0 -0
  51. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/hooks/worktree_guard.py +0 -0
  52. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/memory/.gitkeep +0 -0
  53. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/permission_rules.json +0 -0
  54. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/rules/code-review-checklist.md +0 -0
  55. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/rules/promoted/index.md +0 -0
  56. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/rules/security-review-checklist.md +0 -0
  57. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/code-review/SKILL.md +0 -0
  58. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/dev-workflow/SKILL.md +0 -0
  59. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/develop/SKILL.md +0 -0
  60. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/doc/SKILL.md +0 -0
  61. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/extract-lib/SKILL.md +0 -0
  62. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/init-session/SKILL.md +0 -0
  63. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/mcp-config/SKILL.md +0 -0
  64. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/parallel-agents/SKILL.md +0 -0
  65. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/pattern-status/SKILL.md +0 -0
  66. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/promote-pattern/SKILL.md +0 -0
  67. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/report-timestamp/SKILL.md +0 -0
  68. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
  69. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/setup/SKILL.md +0 -0
  70. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/start/SKILL.md +0 -0
  71. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/skills/task-routing/SKILL.md +0 -0
  72. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.claude/state/.gitkeep +0 -0
  73. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/.gitignore +0 -0
  74. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/LICENSE +0 -0
  75. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/README.md +0 -0
  76. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/hatch_build.py +0 -0
  77. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/pyproject.toml +0 -0
  78. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/__main__.py +0 -0
  79. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/_excludes.py +0 -0
  80. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/_terminal.py +0 -0
  81. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli.py +0 -0
  82. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli_doctor.py +0 -0
  83. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli_init.py +0 -0
  84. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli_list.py +0 -0
  85. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli_plan.py +0 -0
  86. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/cli_update.py +0 -0
  87. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/paths.py +0 -0
  88. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/plan_validator.py +0 -0
  89. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/src/c3/platforms.py +0 -0
  90. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/__init__.py +0 -0
  91. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/conftest.py +0 -0
  92. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/__init__.py +0 -0
  93. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
  94. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_post_tool.py +0 -0
  95. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_pre_tool.py +0 -0
  96. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_record_tier_outcome.py +0 -0
  97. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_restore_session.py +0 -0
  98. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_review_hint_inject.py +0 -0
  99. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_select_tier_escalation.py +0 -0
  100. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_start.py +0 -0
  101. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_stop.py +0 -0
  102. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_utils.py +0 -0
  103. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
  104. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_similarity_boost.py +0 -0
  105. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_statusline.py +0 -0
  106. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_statusline_template_sync.py +0 -0
  107. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_subagent_log.py +0 -0
  108. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_sync_check.py +0 -0
  109. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/hooks/test_template_guard.py +0 -0
  110. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/skills/__init__.py +0 -0
  111. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/skills/test_session_backlog_reconciliation.py +0 -0
  112. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
  113. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
  114. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/skills/test_task_routing_skill.py +0 -0
  115. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_adapters.py +0 -0
  116. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_cli_init.py +0 -0
  117. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_cli_list.py +0 -0
  118. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_cli_plan.py +0 -0
  119. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_cli_tier.py +0 -0
  120. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_docstring_consistency.py +0 -0
  121. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_excludes.py +0 -0
  122. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_paths.py +0 -0
  123. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_plan_validator.py +0 -0
  124. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_pre_compact.py +0 -0
  125. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_pre_tool_hook.py +0 -0
  126. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_precompact_additional.py +0 -0
  127. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_precompact_toctou_fixes.py +0 -0
  128. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_session_utils_additional.py +0 -0
  129. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_statusline.py +0 -0
  130. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_stop_additional.py +0 -0
  131. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_stop_hook.py +0 -0
  132. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_stop_precompact_fixes.py +0 -0
  133. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_sync_template_stop.py +0 -0
  134. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_template_pre_tool_hook.py +0 -0
  135. {claude_code_conductor-2.5.0 → claude_code_conductor-2.6.1}/tests/test_worktree_guard.py +0 -0
@@ -193,9 +193,12 @@ def build_summary_markdown(
193
193
  """集約結果の Markdown を組み立てる。"""
194
194
  if today is None:
195
195
  today = datetime.now(timezone.utc)
196
- today_str = today.date().isoformat() if isinstance(today, datetime) else str(today)
197
- start_str = (today.date() - timedelta(days=window_days - 1)).isoformat() \
198
- if isinstance(today, datetime) else str(today)
196
+ elif not isinstance(today, datetime):
197
+ today = datetime.combine(today, datetime.min.time(), tzinfo=timezone.utc)
198
+ if today.tzinfo is None:
199
+ today = today.replace(tzinfo=timezone.utc)
200
+ today_str = today.date().isoformat()
201
+ start_str = (today.date() - timedelta(days=window_days - 1)).isoformat()
199
202
 
200
203
  lines: list[str] = [
201
204
  "# 集約サマリ",
@@ -527,8 +530,19 @@ def _atomic_write(output_path: str, payload: str) -> bool:
527
530
 
528
531
 
529
532
  def _escape_for_xml(text: str) -> str:
530
- """XML タグ境界突破を防ぐためタグ記号をエンティティに変換する。[SR-AI-001]"""
531
- return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
533
+ """XML タグ境界突破を防ぐためタグ記号・引用符をエンティティに変換する。[SR-AI-001]
534
+
535
+ 引用符 (" / ') もエスケープすることで、属性値混入による境界突破を防ぐ。
536
+ 変換順: & を最初に変換してから他の文字を変換する(二重エスケープ防止)。
537
+ """
538
+ return (
539
+ text
540
+ .replace("&", "&amp;")
541
+ .replace("<", "&lt;")
542
+ .replace(">", "&gt;")
543
+ .replace('"', "&quot;")
544
+ .replace("'", "&#39;")
545
+ )
532
546
 
533
547
 
534
548
  def _build_llm_prompt(
@@ -649,8 +663,13 @@ def build_llm_summary_section(
649
663
  )
650
664
 
651
665
  try:
666
+ # セキュリティ設計 [SR-AI-001]:
667
+ # --dangerously-skip-permissions は全ツールへのフルアクセスを付与するため除去。
668
+ # LLM 要約生成はテキスト出力のみで十分なので --tools "" で全ツールを無効化する。
669
+ # これにより子 claude プロセスからのファイル読み書き・Bash 実行が完全にブロックされる。
670
+ # prompt は引数経由でのみ渡し、ファイル書き込みは親プロセス側で行う(職責分離)。
652
671
  result = subprocess.run(
653
- [claude_exe, "-p", prompt, "--dangerously-skip-permissions"],
672
+ [claude_exe, "-p", prompt, "--tools", ""],
654
673
  **run_kwargs,
655
674
  )
656
675
  except subprocess.TimeoutExpired:
@@ -11,6 +11,7 @@ import platform
11
11
  import re
12
12
  import subprocess
13
13
  import sys
14
+ from urllib.parse import urlparse
14
15
 
15
16
  try:
16
17
  sys.stdin.reconfigure(encoding='utf-8')
@@ -24,13 +25,17 @@ _CLAUDE_DIR = os.path.dirname(_HOOKS_DIR)
24
25
  RULES_PATH = os.path.join(_CLAUDE_DIR, 'permission_rules.json')
25
26
 
26
27
  DEFAULT_RULES: dict = {'auto_allow': [], 'notify_on_auto': True}
28
+ _CREATE_NO_WINDOW = 0x08000000
29
+ # p_arg 付きパターンに対してシェル制御文字を含むコマンドの自動承認を防ぐ
30
+ _SHELL_INJECTION_RE = re.compile(r';|&&|\|\||`|\$\(')
27
31
 
28
32
 
29
33
  def notify(message: str) -> None:
30
34
  system = platform.system()
31
35
  try:
32
36
  if system == 'Darwin':
33
- safe = message.replace('\\', '\\\\').replace('"', '\\"')
37
+ safe = message.replace('\n', ' ').replace('\r', ' ')
38
+ safe = safe.replace('\\', '\\\\').replace('"', '\\"')
34
39
  subprocess.run(
35
40
  ['osascript', '-e', f'display notification "{safe}" with title "Claude Code"'],
36
41
  capture_output=True, timeout=5
@@ -41,21 +46,26 @@ def notify(message: str) -> None:
41
46
  capture_output=True, timeout=5
42
47
  )
43
48
  elif system == 'Windows':
44
- safe = re.sub(r'[`$(){}\r\n]', '', message)
45
- safe = safe.replace('"', '`"')
46
- ps = (
49
+ import base64
50
+ # メッセージを UTF-8 → Base64 に変換し、PowerShell スクリプト本文に
51
+ # 生のユーザーデータを含めない。Base64 文字列は英数字と +/= のみで
52
+ # PowerShell インジェクション ([SR-INJ-002]) が物理的に不可能。
53
+ msg_b64 = base64.b64encode(message.encode('utf-8')).decode('ascii')
54
+ ps_script = (
47
55
  'Add-Type -AssemblyName System.Windows.Forms; '
48
56
  '$n = New-Object System.Windows.Forms.NotifyIcon; '
49
57
  '$n.Icon = [System.Drawing.SystemIcons]::Information; '
50
58
  '$n.Visible = $true; '
51
- f'$n.ShowBalloonTip(4000, "Claude Code", "{safe}", '
59
+ f'$msg = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("{msg_b64}")); '
60
+ '$n.ShowBalloonTip(4000, \'Claude Code\', $msg, '
52
61
  '[System.Windows.Forms.ToolTipIcon]::Info); '
53
62
  'Start-Sleep -Milliseconds 4500; '
54
63
  '$n.Dispose()'
55
64
  )
65
+ encoded = base64.b64encode(ps_script.encode('utf-16-le')).decode('ascii')
56
66
  subprocess.Popen(
57
- ['powershell', '-WindowStyle', 'Hidden', '-Command', ps],
58
- creationflags=0x08000000 # CREATE_NO_WINDOW
67
+ ['powershell', '-WindowStyle', 'Hidden', '-EncodedCommand', encoded],
68
+ creationflags=_CREATE_NO_WINDOW
59
69
  )
60
70
  except Exception as e:
61
71
  print(f'[permission_handler] 通知エラー: {e}', file=sys.stderr)
@@ -97,14 +107,21 @@ def matches_pattern(tool_name: str, tool_input: dict, pattern: str) -> bool:
97
107
 
98
108
  # ツール別に照合対象を決定
99
109
  if tool_name == 'Bash':
100
- subject = tool_input.get('command', '')
110
+ command = tool_input.get('command', '')
111
+ if _SHELL_INJECTION_RE.search(command):
112
+ return False
113
+ subject = command
101
114
  elif tool_name in ('Write', 'Edit', 'Read', 'Glob'):
102
115
  subject = tool_input.get('file_path', tool_input.get('pattern', ''))
103
116
  elif tool_name == 'WebFetch':
104
117
  url = tool_input.get('url', '')
105
118
  if p_arg.startswith('domain:'):
106
119
  domain = p_arg[len('domain:'):]
107
- return domain in url
120
+ try:
121
+ host = urlparse(url).hostname or ''
122
+ return host == domain or host.endswith('.' + domain)
123
+ except Exception:
124
+ return False
108
125
  subject = url
109
126
  else:
110
127
  subject = str(tool_input)
@@ -132,6 +149,8 @@ def main() -> None:
132
149
 
133
150
  tool_name = payload.get('tool_name', '')
134
151
  tool_input = payload.get('tool_input', {})
152
+ if not isinstance(tool_input, dict):
153
+ tool_input = {}
135
154
  rules = load_rules()
136
155
  description = describe_tool(tool_name, tool_input)
137
156
 
@@ -43,6 +43,7 @@ _BINARY_SAMPLE_BYTES = 8 * 1024
43
43
  # applicable_extensions が None なら全対象拡張子に適用。
44
44
  _QUALITY_PATTERNS: list[tuple[str, "re.Pattern[str]", "frozenset[str] | None"]] = [
45
45
  ('console.log', re.compile(r'console\.log\('), frozenset({'.js', '.ts', '.tsx', '.jsx'})),
46
+ # Python の print() のみ対象。他言語の print は別パターン名で追加すること
46
47
  ('print', re.compile(r'^\s*print\('), frozenset({'.py'})),
47
48
  ('TODO', re.compile(r'\bTODO\b'), None),
48
49
  ('FIXME', re.compile(r'\bFIXME\b'), None),
@@ -5,7 +5,6 @@ restore_session.py: SessionStart(compact) hook.
5
5
  """
6
6
 
7
7
  import os
8
- import re
9
8
  import sys
10
9
 
11
10
  try:
@@ -20,6 +19,19 @@ _CLAUDE_DIR = os.path.dirname(_HOOKS_DIR)
20
19
  SESSIONS_DIR = os.path.join(_CLAUDE_DIR, 'memory', 'sessions')
21
20
 
22
21
 
22
+ def _load_session_utils():
23
+ """session_utils モジュールを動的にロードして返す(同階層)。"""
24
+ import importlib.util
25
+
26
+ util_path = os.path.join(_HOOKS_DIR, "session_utils.py")
27
+ spec = importlib.util.spec_from_file_location("session_utils", util_path)
28
+ if spec is None or spec.loader is None:
29
+ raise ImportError(f"session_utils が見つかりません: {util_path}")
30
+ module = importlib.util.module_from_spec(spec)
31
+ spec.loader.exec_module(module) # type: ignore[attr-defined]
32
+ return module
33
+
34
+
23
35
  def find_latest_session() -> str | None:
24
36
  if not os.path.isdir(SESSIONS_DIR):
25
37
  return None
@@ -29,14 +41,6 @@ def find_latest_session() -> str | None:
29
41
  return os.path.join(SESSIONS_DIR, max(files))
30
42
 
31
43
 
32
- def extract_section(content: str, heading: str) -> str:
33
- pattern = rf'## {re.escape(heading)}\n(.*?)(?=\n## |\n<!--|\Z)'
34
- match = re.search(pattern, content, re.DOTALL)
35
- if not match:
36
- return ''
37
- return match.group(1).strip()
38
-
39
-
40
44
  def main():
41
45
  path = find_latest_session()
42
46
  if not path or not os.path.exists(path):
@@ -45,6 +49,9 @@ def main():
45
49
  with open(path, 'r', encoding='utf-8') as f:
46
50
  content = f.read()
47
51
 
52
+ session_utils = _load_session_utils()
53
+ extract_section = session_utils.extract_section
54
+
48
55
  date_str = os.path.basename(path).replace('.tmp', '')
49
56
  todos = extract_section(content, '残タスク')
50
57
  successes = extract_section(content, 'うまくいったアプローチ')
@@ -25,6 +25,7 @@ from __future__ import annotations
25
25
  import os
26
26
  import re
27
27
  import sys
28
+ import tempfile
28
29
  from datetime import datetime, timedelta, timezone
29
30
  from pathlib import Path
30
31
 
@@ -164,17 +165,24 @@ def append_hints_to_report(
164
165
  return False
165
166
 
166
167
  new_text = text.rstrip() + "\n\n" + hint_section
168
+ fd, tmp_path = tempfile.mkstemp(prefix=".tmp_", dir=report_path.parent)
167
169
  try:
168
- report_path.write_text(new_text, encoding="utf-8")
170
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
171
+ f.write(new_text)
172
+ os.replace(tmp_path, report_path)
169
173
  except OSError as exc:
170
174
  print(f"[review_hint_inject] failed to write {report_path}: {exc}", file=sys.stderr)
175
+ try:
176
+ if os.path.exists(tmp_path):
177
+ os.unlink(tmp_path)
178
+ except OSError:
179
+ pass
171
180
  return False
172
181
  return True
173
182
 
174
183
 
175
184
  def collect_decisions_for_report(report_text: str) -> dict[str, list[dict]]:
176
185
  """レポート内の checklist_id を全て抽出し、各 ID の過去判断を取得する。"""
177
- _ensure_c3_db_path_in_sys_path()
178
186
  try:
179
187
  from c3 import db as c3_db # noqa: PLC0415
180
188
  except ImportError as exc:
@@ -29,6 +29,7 @@ import hashlib
29
29
  import json
30
30
  import os
31
31
  import random
32
+ import re
32
33
  import sys
33
34
  from pathlib import Path
34
35
 
@@ -40,8 +41,13 @@ except AttributeError:
40
41
  pass
41
42
 
42
43
 
43
- # 学習データ収集期の閾値(合計試行数がこの値未満なら uniform 選択)
44
- LEARNING_THRESHOLD = 30
44
+ # 学習データ収集期の閾値(合計試行数がこの値未満なら uniform 選択)。
45
+ # SSOT: c3.db.LEARNING_THRESHOLD から取得し、import 失敗時はフォールバック値 30 を使う(CR-M-002)。
46
+ try:
47
+ from c3 import db as _c3_db_const # type: ignore[import-not-found]
48
+ LEARNING_THRESHOLD: int = _c3_db_const.LEARNING_THRESHOLD
49
+ except ImportError:
50
+ LEARNING_THRESHOLD = 30
45
51
 
46
52
  # 複雑度推定のキーワード
47
53
  SIMPLE_KEYWORDS = frozenset({
@@ -68,6 +74,33 @@ _PROMPT_PREFIX_MAX = 200
68
74
  SIMILARITY_STRONG_THRESHOLD = 0.8 # この値以上で complexity を上書き
69
75
  SIMILARITY_WEAK_THRESHOLD = 0.6 # この値以上で信頼度補強のみ
70
76
 
77
+ # prompt 保存前のマスク処理: pre_tool.py の _SECRET_PATTERNS と同等のパターン。
78
+ # 検出した値を *** に置換してから保存することで二次漏洩を防ぐ。
79
+ # NOTE: ここで値をマスクすることで類似度推定の精度も若干下がる可能性があるが、
80
+ # セキュリティ優先として許容する(設計書に記載なし: SR-V-001 対応と判断)。
81
+ _MASK_PATTERNS: list[re.Pattern[str]] = [
82
+ re.compile(r'(password=)\S+', re.IGNORECASE),
83
+ re.compile(r'(api[_-]?key=)\S+', re.IGNORECASE),
84
+ re.compile(r'(Bearer\s+)[\w\-\.]+', re.IGNORECASE),
85
+ re.compile(r'(\btoken=)\S+', re.IGNORECASE),
86
+ re.compile(r'(\bsecret=)\S+', re.IGNORECASE),
87
+ re.compile(r'(aws_secret_access_key=)\S+', re.IGNORECASE),
88
+ re.compile(r'(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)'),
89
+ ]
90
+
91
+
92
+ def _mask_secrets(text: str) -> str:
93
+ """秘密情報パターンにマッチする値部分を *** に置換して返す。
94
+
95
+ キー名やプレフィックスは残し、値のみを置換することで
96
+ 「何が含まれていたか」は伝わらないようにする。
97
+ PEM ブロックは開始タグ〜終了タグ全体を *** に置換する。
98
+ """
99
+ result = text
100
+ for pattern in _MASK_PATTERNS:
101
+ result = pattern.sub(lambda m: m.group(1) + "***", result)
102
+ return result
103
+
71
104
  # prompt-history.jsonl の末尾から読む最大行数(パフォーマンス対策)
72
105
  _PROMPT_HISTORY_SCAN_LINES = 1000
73
106
 
@@ -78,8 +111,10 @@ def _prompt_prefix_and_hash(prompt: str) -> tuple[str, str]:
78
111
  """prompt から (prefix, hash) を抽出する。
79
112
 
80
113
  Phase 2-C: prefix 200 文字 + SHA256 先頭 16 文字(プライバシー対策)。
114
+ SR-V-001: prefix に含まれる秘密情報パターンは *** にマスクしてから保存する。
115
+ hash はマスク前の原文から計算する(一意性を保つため)。
81
116
  """
82
- prefix = prompt[:_PROMPT_PREFIX_MAX]
117
+ prefix = _mask_secrets(prompt[:_PROMPT_PREFIX_MAX])
83
118
  h = hashlib.sha256(prompt.encode("utf-8", errors="replace")).hexdigest()[:16]
84
119
  return prefix, h
85
120
 
@@ -228,6 +263,17 @@ _ESCALATION_MAP: dict[str, str] = {
228
263
  ESCALATION_THRESHOLD = 0.5
229
264
 
230
265
 
266
+ def _db_failure_rate(complexity: str, tier: str) -> tuple:
267
+ """DB から failure rate を読み取るデフォルト実装。
268
+
269
+ c3_db のインポートに失敗した場合は ``(None, 0)`` を返す。
270
+ """
271
+ c3_db = _load_c3_db_module()
272
+ if c3_db is None:
273
+ return None, 0
274
+ return c3_db.read_tier_failure_rate(complexity, tier)
275
+
276
+
231
277
  def maybe_escalate(
232
278
  complexity: str,
233
279
  chosen_tier: str,
@@ -241,7 +287,7 @@ def maybe_escalate(
241
287
  chosen_tier: select_tier が選んだ tier。
242
288
  failure_rate_fn: テスト用に注入可能な
243
289
  ``(complexity, tier) -> (rate_or_None, sample_count)``。
244
- 省略時は :func:`c3_db.read_tier_failure_rate` を使う。
290
+ 省略時は :func:`_db_failure_rate` を使う。
245
291
 
246
292
  Returns:
247
293
  ``(effective_tier, escalation_reason)``。
@@ -251,17 +297,8 @@ def maybe_escalate(
251
297
  if chosen_tier not in _ESCALATION_MAP:
252
298
  return chosen_tier, None
253
299
 
254
- if failure_rate_fn is None:
255
- # 既定の DB ヘルパーを呼ぶ。
256
- c3_db = _load_c3_db_module()
257
- if c3_db is None:
258
- return chosen_tier, None
259
-
260
- def _db_failure_rate(complexity: str, tier: str) -> tuple:
261
- return c3_db.read_tier_failure_rate(complexity, tier)
262
- failure_rate_fn = _db_failure_rate
263
-
264
- rate, samples = failure_rate_fn(complexity, chosen_tier)
300
+ effective_fn = failure_rate_fn or _db_failure_rate
301
+ rate, samples = effective_fn(complexity, chosen_tier)
265
302
  if rate is None or rate < ESCALATION_THRESHOLD:
266
303
  return chosen_tier, None
267
304
 
@@ -73,8 +73,7 @@ def extract_section(content: str, heading: str) -> str:
73
73
  セクション本文(前後の空白除去済み)、または空文字列。
74
74
 
75
75
  Notes:
76
- restore_session.py には独自実装の同名関数があり、後方互換性のため
77
- 当面そのまま残す。新規コード(consolidate_memory.py 等)は本関数を使う。
76
+ 新規コード(consolidate_memory.py 等)は本関数を使う。
78
77
  """
79
78
  pattern = rf'## {re.escape(heading)}\n(.*?)(?=\n## |\n<!--|\Z)'
80
79
  match = re.search(pattern, content, re.DOTALL)
@@ -141,10 +141,15 @@ def extract_session_patterns(date_str: str) -> list:
141
141
 
142
142
 
143
143
  def _parse_session_date(date_str: str):
144
+ """Parse a yyyymmdd date string and return a date object, or None if invalid.
145
+
146
+ Returns None (rather than a sentinel like date.min) so callers can explicitly
147
+ filter out unparseable entries instead of silently treating them as very old.
148
+ """
144
149
  try:
145
150
  return datetime.strptime(date_str, '%Y%m%d').date()
146
151
  except ValueError:
147
- return date.min
152
+ return None
148
153
 
149
154
 
150
155
  def _build_sessions_by_date(sessions_dir: str) -> set[str]:
@@ -164,9 +169,14 @@ def count_sessions_since(registered_date_str: str, sessions_by_date: set[str] |
164
169
  return 1
165
170
  sessions_by_date = _build_sessions_by_date(SESSIONS_DIR)
166
171
  registered = _parse_session_date(registered_date_str)
172
+ # If registered_date_str itself is unparseable, we cannot determine a baseline;
173
+ # fall back to counting all sessions rather than silently returning a wrong value.
174
+ if registered is None:
175
+ return max(len(sessions_by_date), 1)
167
176
  count = sum(
168
177
  1 for d in sessions_by_date
169
- if _parse_session_date(d) >= registered
178
+ # Skip session entries whose date string is malformed (parsed as None).
179
+ if (parsed := _parse_session_date(d)) is not None and parsed >= registered
170
180
  )
171
181
  return max(count, 1)
172
182
 
@@ -45,6 +45,7 @@
45
45
  "Bash(git push origin v*)",
46
46
  "Bash(gh release create v*)",
47
47
  "Bash(gh release view v*)",
48
+ "Bash(codex exec*)",
48
49
  "WebSearch"
49
50
  ],
50
51
  "deny": [
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: codex-review
3
+ description: |
4
+ Codex CLI に .codex/agents/ のエージェント定義を読み込ませ、
5
+ code-reviewer または security-reviewer のペルソナとしてコードレビューを実行するスキル。
6
+ C3 Codex アダプター(.codex/ ディレクトリと AGENTS.md)がセットアップ済みの場合のみ有効。
7
+ 通常の C3 code-reviewer / security-reviewer と同じレポート契約([CR-XX-NNN] / [SR-XX-NNN])を維持する。
8
+
9
+ 【単一ファイルモード】特定ファイルを Codex でレビューする:
10
+ args: "code-reviewer src/path/to/file.py"
11
+ args: "security-reviewer src/path/to/file.py"
12
+
13
+ 【ワークフローモード】git diff の変更全体を Codex でレビューする(通常ワークフローとの並走用):
14
+ args: "workflow code-reviewer"
15
+ args: "workflow security-reviewer"
16
+
17
+ 呼び出しトリガー:
18
+ - 「Codex でレビューして」「Codex に code-reviewer をやらせて」
19
+ - 「codex-review」「/codex-review」
20
+ - 「Codex でセキュリティレビュー」
21
+ - 「Codex も並列でレビューさせて」「ワークフローで Codex レビュー」
22
+ ---
23
+
24
+ # codex-review
25
+
26
+ `.codex/agents/{reviewer_type}.toml` のエージェント定義を `codex exec` のプロンプトに埋め込み、
27
+ Codex 自身が code-reviewer / security-reviewer ペルソナとしてレビューを実行する。
28
+
29
+ 2つのモードがある:
30
+ - **単一ファイルモード**: 指定ファイルを直接レビュー
31
+ - **ワークフローモード**: `git diff HEAD` の変更差分を対象にレビュー(通常ワークフローの Claude レビューと並走させる想定)
32
+
33
+ ---
34
+
35
+ ## 前提確認
36
+
37
+ Glob で `.codex/agents/code-reviewer.toml` を確認する。
38
+
39
+ 存在しない場合は以下を表示してスキルを終了する:
40
+ ```
41
+ [codex-review] Codex アダプターがセットアップされていません。
42
+ 先に `c3 init --platform codex` を実行してください。
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Step 1: モードとレビュー設定を確認する
48
+
49
+ args を解析する:
50
+ - `"workflow code-reviewer"` → ワークフローモード + code-reviewer
51
+ - `"workflow security-reviewer"` → ワークフローモード + security-reviewer
52
+ - `"code-reviewer src/path/file.py"` → 単一ファイルモード + code-reviewer
53
+ - `"security-reviewer src/path/file.py"` → 単一ファイルモード + security-reviewer
54
+
55
+ args が不十分な場合、AskUserQuestion でレビュー種別とモードを確認する:
56
+
57
+ ```json
58
+ {
59
+ "questions": [
60
+ {
61
+ "question": "実行するレビューの種類を選択してください",
62
+ "header": "レビュー種別",
63
+ "multiSelect": false,
64
+ "options": [
65
+ { "label": "code-reviewer", "description": "品質・保守性・パフォーマンスをレビュー" },
66
+ { "label": "security-reviewer", "description": "OWASP Top 10 基準でセキュリティ脆弱性をレビュー" }
67
+ ]
68
+ },
69
+ {
70
+ "question": "レビュー対象を選択してください",
71
+ "header": "対象",
72
+ "multiSelect": false,
73
+ "options": [
74
+ { "label": "ワークフロー(git diff)", "description": "現在の変更差分全体をレビュー。通常ワークフローと並走させる場合はこちら" },
75
+ { "label": "単一ファイル", "description": "特定のファイルを指定してレビュー" }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ 単一ファイルモードでファイルパスが未指定の場合:
83
+
84
+ ```json
85
+ {
86
+ "questions": [{
87
+ "question": "レビュー対象のファイルパスを入力してください(「その他」から入力)",
88
+ "header": "対象ファイル",
89
+ "multiSelect": false,
90
+ "options": [
91
+ { "label": "その他(自由入力)", "description": "例: src/c3/cli_ask.py" }
92
+ ]
93
+ }]
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Step 2: レビュー対象のコンテンツを取得する
100
+
101
+ ### ワークフローモードの場合
102
+
103
+ Bash で以下を実行する:
104
+
105
+ ```bash
106
+ git diff HEAD --stat
107
+ ```
108
+
109
+ 変更ファイルがない場合は `git diff HEAD~1 --stat` を試す。
110
+ それも空の場合は「変更差分が見つかりません。コミット済みの変更を対象にするには `git diff HEAD~1` が必要です」と表示して終了する。
111
+
112
+ 続けて差分本体を取得する:
113
+
114
+ ```bash
115
+ git diff HEAD
116
+ ```
117
+
118
+ 差分が長い場合(目安 200 行超)は先頭 200 行に制限する:
119
+
120
+ ```bash
121
+ git diff HEAD | head -200
122
+ ```
123
+
124
+ 取得した内容を `{review_target}` として保持する。対象説明文は「git diff HEAD の変更差分」とする。
125
+
126
+ ### 単一ファイルモードの場合
127
+
128
+ 指定パスを Read してファイル内容を `{review_target}` として保持する。
129
+ 対象説明文はファイルパスとする。
130
+
131
+ ---
132
+
133
+ ## Step 3: エージェント定義を Read する
134
+
135
+ `.codex/agents/{reviewer_type}.toml` を Read して `{agent_toml}` として保持する。
136
+
137
+ ---
138
+
139
+ ## Step 4: タイムスタンプを取得する
140
+
141
+ Skill ツールで `report-timestamp` を呼び出して `{timestamp}` を取得する。
142
+
143
+ レポートファイル名:
144
+ - code-reviewer: `code-review-report-{timestamp}.md`
145
+ - security-reviewer: `security-review-report-{timestamp}.md`
146
+
147
+ ---
148
+
149
+ ## Step 5: codex exec を実行する
150
+
151
+ Bash で以下を実行する(`--sandbox workspace-write`)。
152
+
153
+ ```bash
154
+ codex exec "以下の定義に従ってエージェントとして動作してください。
155
+
156
+ === エージェント定義(.codex/agents/{reviewer_type}.toml)===
157
+ {agent_toml}
158
+ === エージェント定義ここまで ===
159
+
160
+ 上記の定義に従い、以下のコードをレビューしてください。
161
+ ファイルシステムへのアクセスが必要な場合(チェックリストの参照など)は Read ツールを使用してください。
162
+
163
+ 対象: {対象説明文}
164
+
165
+ {review_target}" --sandbox workspace-write 2>&1
166
+ ```
167
+
168
+ 出力を `{codex_output}` として保持する。
169
+
170
+ ---
171
+
172
+ ## Step 6: レポートを Write する
173
+
174
+ `.claude/reports/{report_filename}` に Write する:
175
+
176
+ ```markdown
177
+ # {reviewer_type} Report (Codex)
178
+
179
+ **対象:** {対象説明文}
180
+ **実行エンジン:** Codex CLI (gpt-5.5) / ペルソナ: {reviewer_type}
181
+ **実行日時:** {timestamp}
182
+
183
+ ---
184
+
185
+ {codex_output}
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Step 7: 結果を表示してフォローアップを確認する
191
+
192
+ レポートの内容を表示し、保存先パスを伝える。
193
+
194
+ AskUserQuestion で確認する:
195
+
196
+ ```json
197
+ {
198
+ "questions": [{
199
+ "question": "Codex レビュー結果を確認してください。次のアクションを選択してください。",
200
+ "header": "次のアクション",
201
+ "multiSelect": false,
202
+ "options": [
203
+ { "label": "確認完了", "description": "レポートを確認した" },
204
+ { "label": "別ファイルも続けてレビューする", "description": "Step 1 から再実行する" },
205
+ { "label": "C3 レビューフローへ引き継ぐ", "description": "code-review スキルへ引き継いでフェーズ E を実行する" }
206
+ ]
207
+ }]
208
+ }
209
+ ```
210
+
211
+ 「C3 レビューフローへ引き継ぐ」が選択された場合は Skill ツールで `code-review` を呼び出す。