claude-code-conductor 2.6.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.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/consolidate_memory.py +25 -6
  2. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/permission_handler.py +6 -2
  3. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/post_tool.py +1 -0
  4. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/restore_session.py +16 -9
  5. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/review_hint_inject.py +10 -2
  6. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/select_tier.py +52 -15
  7. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_utils.py +1 -2
  8. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/stop.py +12 -2
  9. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/CHANGELOG.md +33 -0
  10. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/PKG-INFO +1 -1
  11. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/__init__.py +1 -1
  12. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/adapters.py +6 -1
  13. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_tier.py +6 -27
  14. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/db.py +55 -1
  15. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/mcp_server.py +9 -1
  16. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/question.py +6 -2
  17. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_consolidate_memory.py +168 -0
  18. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_planner_check.py +96 -0
  19. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_select_tier.py +96 -0
  20. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_ask.py +69 -0
  21. claude_code_conductor-2.6.1/tests/test_mcp_server_elicit.py +113 -0
  22. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/CLAUDE.md +0 -0
  23. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/architect.md +0 -0
  24. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/code-reviewer.md +0 -0
  25. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/developer.md +0 -0
  26. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/doc-writer.md +0 -0
  27. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/interviewer.md +0 -0
  28. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/planner.md +0 -0
  29. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/project-setup.md +0 -0
  30. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/security-reviewer.md +0 -0
  31. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/systematic-debugger.md +0 -0
  32. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/tester.md +0 -0
  33. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_developer.md +0 -0
  34. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_systematic-debugger.md +0 -0
  35. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_tester.md +0 -0
  36. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/docs/platform-adapters.md +0 -0
  37. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/docs/settings.json.md +0 -0
  38. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/pre_compact.py +0 -0
  39. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/pre_tool.py +0 -0
  40. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/record_review_decision.py +0 -0
  41. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/record_tier_outcome.py +0 -0
  42. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/schema.sql +0 -0
  43. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_start.py +0 -0
  44. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_stop.py +0 -0
  45. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/statusline.py +0 -0
  46. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/subagent_log.py +0 -0
  47. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/worktree_guard.py +0 -0
  48. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/memory/.gitkeep +0 -0
  49. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/permission_rules.json +0 -0
  50. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/rules/code-review-checklist.md +0 -0
  51. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/rules/promoted/index.md +0 -0
  52. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/rules/security-review-checklist.md +0 -0
  53. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/settings.json +0 -0
  54. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/code-review/SKILL.md +0 -0
  55. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/codex-review/SKILL.md +0 -0
  56. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/dev-workflow/SKILL.md +0 -0
  57. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/develop/SKILL.md +0 -0
  58. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/doc/SKILL.md +0 -0
  59. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/extract-lib/SKILL.md +0 -0
  60. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/init-session/SKILL.md +0 -0
  61. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/mcp-config/SKILL.md +0 -0
  62. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/parallel-agents/SKILL.md +0 -0
  63. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/pattern-status/SKILL.md +0 -0
  64. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/promote-pattern/SKILL.md +0 -0
  65. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/report-timestamp/SKILL.md +0 -0
  66. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
  67. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/setup/SKILL.md +0 -0
  68. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/start/SKILL.md +0 -0
  69. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/task-routing/SKILL.md +0 -0
  70. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/state/.gitkeep +0 -0
  71. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.gitignore +0 -0
  72. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/LICENSE +0 -0
  73. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/README.md +0 -0
  74. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/hatch_build.py +0 -0
  75. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/pyproject.toml +0 -0
  76. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/__main__.py +0 -0
  77. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/_excludes.py +0 -0
  78. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/_terminal.py +0 -0
  79. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli.py +0 -0
  80. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_ask.py +0 -0
  81. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_doctor.py +0 -0
  82. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_init.py +0 -0
  83. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_list.py +0 -0
  84. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_plan.py +0 -0
  85. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_update.py +0 -0
  86. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/paths.py +0 -0
  87. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/plan_validator.py +0 -0
  88. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/platforms.py +0 -0
  89. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/__init__.py +0 -0
  90. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/conftest.py +0 -0
  91. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/__init__.py +0 -0
  92. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_permission_handler.py +0 -0
  93. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
  94. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_post_tool.py +0 -0
  95. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_pre_tool.py +0 -0
  96. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_record_tier_outcome.py +0 -0
  97. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_restore_session.py +0 -0
  98. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_review_hint_inject.py +0 -0
  99. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_select_tier_escalation.py +0 -0
  100. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_start.py +0 -0
  101. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_stop.py +0 -0
  102. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_utils.py +0 -0
  103. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
  104. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_similarity_boost.py +0 -0
  105. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_statusline.py +0 -0
  106. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_statusline_template_sync.py +0 -0
  107. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_subagent_log.py +0 -0
  108. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_sync_check.py +0 -0
  109. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_template_guard.py +0 -0
  110. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/__init__.py +0 -0
  111. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_session_backlog_reconciliation.py +0 -0
  112. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
  113. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
  114. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_task_routing_skill.py +0 -0
  115. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_adapters.py +0 -0
  116. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_init.py +0 -0
  117. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_list.py +0 -0
  118. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_plan.py +0 -0
  119. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_tier.py +0 -0
  120. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_docstring_consistency.py +0 -0
  121. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_excludes.py +0 -0
  122. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_paths.py +0 -0
  123. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_plan_validator.py +0 -0
  124. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_pre_compact.py +0 -0
  125. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_pre_tool_hook.py +0 -0
  126. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_precompact_additional.py +0 -0
  127. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_precompact_toctou_fixes.py +0 -0
  128. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_session_utils_additional.py +0 -0
  129. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_statusline.py +0 -0
  130. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_stop_additional.py +0 -0
  131. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_stop_hook.py +0 -0
  132. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_stop_precompact_fixes.py +0 -0
  133. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_sync_template_stop.py +0 -0
  134. {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_template_pre_tool_hook.py +0 -0
  135. {claude_code_conductor-2.6.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:
@@ -47,13 +47,17 @@ def notify(message: str) -> None:
47
47
  )
48
48
  elif system == 'Windows':
49
49
  import base64
50
- safe_msg = re.sub(r"['\r\n\x00-\x1f\x7f]", '', message)[:200]
50
+ # メッセージを UTF-8 Base64 に変換し、PowerShell スクリプト本文に
51
+ # 生のユーザーデータを含めない。Base64 文字列は英数字と +/= のみで
52
+ # PowerShell インジェクション ([SR-INJ-002]) が物理的に不可能。
53
+ msg_b64 = base64.b64encode(message.encode('utf-8')).decode('ascii')
51
54
  ps_script = (
52
55
  'Add-Type -AssemblyName System.Windows.Forms; '
53
56
  '$n = New-Object System.Windows.Forms.NotifyIcon; '
54
57
  '$n.Icon = [System.Drawing.SystemIcons]::Information; '
55
58
  '$n.Visible = $true; '
56
- f"$n.ShowBalloonTip(4000, 'Claude Code', '{safe_msg}', "
59
+ f'$msg = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("{msg_b64}")); '
60
+ '$n.ShowBalloonTip(4000, \'Claude Code\', $msg, '
57
61
  '[System.Windows.Forms.ToolTipIcon]::Info); '
58
62
  'Start-Sleep -Milliseconds 4500; '
59
63
  '$n.Dispose()'
@@ -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
 
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.1] - 2026-05-15
4
+
5
+ ### 概要
6
+
7
+ security-audit による定期監査の結果をフィックス。セキュリティ強化 3 件・コード品質修正 14 件を適用。API 変更なし。
8
+
9
+ ### 修正(セキュリティ)
10
+
11
+ - **[SR-INJ-002] `permission_handler.py` Windows 通知を Base64 EncodedCommand 化**: `safe_msg` を f-string に直接埋め込んでいた方式を廃止し、`base64.b64encode` で変換してから `-EncodedCommand` に渡すよう変更。`'` / バッククォート / `$` を含むメッセージでもインジェクション不可能になった
12
+ - **[SR-AI-001] `consolidate_memory.py` LLM 子プロセスの攻撃面縮小**: `_escape_for_xml` に引用符エスケープ(`"` → `&quot;`、`'` → `&#39;`)を追加。claude 子プロセスの `--dangerously-skip-permissions` を除去し `--tools ""` で全ツール無効化
13
+ - **[SR-V-001] `select_tier.py` prompt_prefix の秘密情報マスク**: プロンプト先頭 200 文字を `.claude/logs/prompt-history.jsonl` に保存する前に、API キー・トークン・パスワード相当の 7 パターンを `***` でマスク
14
+
15
+ ### 修正
16
+
17
+ - **[CR-Q-004] `db.py` / `cli_tier.py` `_BUSY_TIMEOUT_MS` を SSOT に統一**: `db.py` を SSOT として `cli_tier.py` の独立定義を削除。`read_recent_outcomes` ヘルパーを `db.py` に追加し `sqlite3.connect` 直接呼び出しを廃止
18
+ - **[CR-M-002] `LEARNING_THRESHOLD` を `db.py` SSOT に統一**: `cli_tier.py` と `select_tier.py` の独立定義を削除。フックのスタンドアロン制約に対応するため `select_tier.py` はダイナミックインポート + フォールバック方式を採用
19
+ - **[CR-M-001] `restore_session.py` の重複 `extract_section` を削除**: `session_utils.extract_section` をダイナミックインポートで参照し、内製実装を削除
20
+ - **[CR-E-003] `review_hint_inject.py` レポート書き込みをアトミック化**: `write_text` を `tempfile.mkstemp` + `os.replace` パターンに変更し、書き込み途中での中断によるファイル破損を防止
21
+ - **[CR-T-001] `mcp_server.py` `_elicit` に `json.JSONDecodeError` ハンドリング追加**: 不正 JSON 行を受信してもメソッドが例外で終了せず、ログ出力してスキップしてループ継続するよう修正
22
+ - **[CR-CT-001] `question.py` `load_questions` の型分岐を明示化**: `isinstance(source, dict)` → `isinstance(source, (str, Path))` → `TypeError` の順に明示化し、`Path(str(dict))` の duck typing を排除
23
+ - **[CR-N-004] `stop.py` `_parse_session_date` の `None` 返却化**: `date.min` センチネル値から `None` 返却に変更し意図を明示。呼び出し元 `count_sessions_since` に `None` フィルタを追加
24
+ - **[CR-Q-002] `consolidate_memory.py` `build_summary_markdown` の today 型統一**: 関数冒頭で `datetime` 型に正規化し、`date` 型で `timespec` 引数が例外になるパスを排除
25
+ - **[CR-M-003] `select_tier.py` `maybe_escalate` の引数上書きを解消**: `_db_failure_rate` をモジュールトップレベルへ抽出し、`effective_fn = failure_rate_fn or _db_failure_rate` で参照
26
+ - **[CR-Q-005] `review_hint_inject.py` 空関数呼び出し削除**: `_ensure_c3_db_path_in_sys_path()` の呼び出しを削除(c3 パッケージは常にインストール済みのため不要)
27
+ - **[CR-M-003] `adapters.py` `_dev_source_pythonpath` に docstring 追加**
28
+ - **[CR-Q-007] `post_tool.py` print パターンスコープのコメント追加**
29
+
30
+ ### セキュリティ告知(環境依存)
31
+
32
+ `pip <= 26.0.1` および `urllib3 <= 2.6.3` に既知脆弱性(CVE-2026-3219 / CVE-2026-6357 / CVE-2026-44431 / CVE-2026-44432)が報告されています。C3 パッケージ自体の直接依存ではありませんが、利用環境で `pip install --upgrade pip urllib3` の実行を推奨します。
33
+
34
+ ---
35
+
3
36
  ## [2.6.0] - 2026-05-15
4
37
 
5
38
  ### 概要
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-conductor
3
- Version: 2.6.0
3
+ Version: 2.6.1
4
4
  Summary: Multi-agent orchestration framework for Claude Code with Codex/Cursor adapters (C3)
5
5
  Project-URL: Homepage, https://github.com/satoh-y-0323/claude-code-conductor
6
6
  Project-URL: Repository, https://github.com/satoh-y-0323/claude-code-conductor
@@ -1,3 +1,3 @@
1
1
  """Claude Code Conductor (C3) - multi-agent orchestration framework for Claude Code."""
2
2
 
3
- __version__ = "2.6.0"
3
+ __version__ = "2.6.1"
@@ -375,7 +375,12 @@ def _toml_multiline_escape(value: str) -> str:
375
375
 
376
376
 
377
377
  def _dev_source_pythonpath() -> Path | None:
378
- """Return ``<repo>/src`` when C3 is running from a source checkout."""
378
+ """Return ``<repo>/src`` when C3 is running from a source checkout.
379
+
380
+ ``src/c3/`` ディレクトリ構造(``__file__`` の親が ``c3``、その親が ``src``)が
381
+ 前提。それ以外のディレクトリ構造の場合、または ``pyproject.toml`` が見つからない
382
+ 場合は ``None`` を返す。
383
+ """
379
384
  here = Path(__file__).resolve()
380
385
  if here.parent.name != "c3" or here.parent.parent.name != "src":
381
386
  return None
@@ -18,7 +18,6 @@ from __future__ import annotations
18
18
  import argparse
19
19
  import json
20
20
  import logging
21
- import sqlite3
22
21
  import sys
23
22
  from typing import Any
24
23
 
@@ -28,9 +27,8 @@ from c3 import db as c3_db
28
27
  logger = logging.getLogger(__name__)
29
28
 
30
29
 
31
- _BUSY_TIMEOUT_MS = 5000
32
30
  _DEFAULT_RECENT_LIMIT = 10
33
- _LEARNING_THRESHOLD = 30 # select_tier.py の LEARNING_THRESHOLD と一致
31
+ _LEARNING_THRESHOLD = c3_db.LEARNING_THRESHOLD # SSOT: db.py で一元管理(CR-M-002)
34
32
  _TIERS = ("haiku", "sonnet", "opus")
35
33
  _COMPLEXITIES = ("simple", "medium", "complex")
36
34
 
@@ -74,7 +72,7 @@ def handle_stats(args: argparse.Namespace) -> int:
74
72
 
75
73
  try:
76
74
  snapshot = _collect_snapshot(db_path, recent_limit=args.recent)
77
- except sqlite3.OperationalError as exc:
75
+ except Exception as exc: # noqa: BLE001
78
76
  print(
79
77
  f"DB アクセスエラー: {exc}\n"
80
78
  "schema_version が古い可能性。新セッションで自動マイグレーションされます。",
@@ -111,29 +109,10 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
111
109
  "expected_success_rate": expected,
112
110
  })
113
111
 
114
- recent_outcomes: list[dict[str, Any]] = []
115
- try:
116
- conn = sqlite3.connect(str(db_path))
117
- try:
118
- conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
119
- rows = conn.execute(
120
- "SELECT task_complexity, tier, success, ts "
121
- "FROM tier_recent_outcomes "
122
- "ORDER BY ts DESC LIMIT ?",
123
- (recent_limit,),
124
- ).fetchall()
125
- finally:
126
- conn.close()
127
- except sqlite3.OperationalError:
128
- rows = []
129
-
130
- for complexity, tier, success, ts in rows:
131
- recent_outcomes.append({
132
- "complexity": complexity,
133
- "tier": tier,
134
- "success": bool(success),
135
- "ts": ts,
136
- })
112
+ recent_outcomes: list[dict[str, Any]] = c3_db.read_recent_outcomes(
113
+ limit=recent_limit,
114
+ db_path=db_path,
115
+ )
137
116
 
138
117
  if total_trials < _LEARNING_THRESHOLD:
139
118
  mode = "uniform"
@@ -28,7 +28,13 @@ logger = logging.getLogger(__name__)
28
28
 
29
29
  # SQLite ロック衝突待機時間(ms)。並列書き込み増加に備えて 5 秒に設定する。
30
30
  # 冪等に各書き込み関数で適用される。
31
- _BUSY_TIMEOUT_MS = 5000
31
+ # 公開定数として export し、cli_tier.py 等の呼び出し側から参照できるようにする(SSOT)。
32
+ BUSY_TIMEOUT_MS = 5000
33
+ _BUSY_TIMEOUT_MS = BUSY_TIMEOUT_MS # 内部互換エイリアス(既存コードへの影響なし)
34
+
35
+ # tier-routing: 学習データ収集期の閾値(合計試行数がこの値未満なら uniform 選択)。
36
+ # SSOT: cli_tier.py / select_tier.py はここから参照する(CR-M-002)。
37
+ LEARNING_THRESHOLD = 30
32
38
 
33
39
 
34
40
  def locate_c3_db(start: Path | None = None) -> Path | None:
@@ -381,6 +387,54 @@ def record_tier_recent_outcome(
381
387
  return False
382
388
 
383
389
 
390
+ def read_recent_outcomes(
391
+ *,
392
+ limit: int = 10,
393
+ db_path: Path | None = None,
394
+ ) -> list[dict]:
395
+ """``tier_recent_outcomes`` から直近 ``limit`` 件を時系列降順で返す。
396
+
397
+ ``cli_tier._collect_snapshot`` の sqlite3 直接呼び出しを置き換えるヘルパー。
398
+ busy_timeout は BUSY_TIMEOUT_MS を冪等に適用する。
399
+
400
+ Args:
401
+ limit: 取得件数の上限(デフォルト 10)。
402
+ db_path: c3.db のパス。省略時は :func:`locate_c3_db` で探索。
403
+
404
+ Returns:
405
+ 各行を ``{"complexity", "tier", "success", "ts"}`` の dict にしたリスト。
406
+ DB 不在 / テーブル不在 / エラー時は空リストを返す(呼び出し側を止めない)。
407
+ """
408
+ if db_path is None:
409
+ db_path = locate_c3_db()
410
+ if db_path is None:
411
+ return []
412
+
413
+ try:
414
+ conn = sqlite3.connect(str(db_path))
415
+ try:
416
+ conn.execute(f"PRAGMA busy_timeout={BUSY_TIMEOUT_MS}")
417
+ rows = conn.execute(
418
+ "SELECT task_complexity, tier, success, ts "
419
+ "FROM tier_recent_outcomes "
420
+ "ORDER BY ts DESC LIMIT ?",
421
+ (limit,),
422
+ ).fetchall()
423
+ finally:
424
+ conn.close()
425
+ except sqlite3.OperationalError as exc:
426
+ logger.debug("read_recent_outcomes: table not found or inaccessible: %s", exc)
427
+ return []
428
+ except Exception as exc: # noqa: BLE001
429
+ logger.warning("read_recent_outcomes: unexpected error: %s", exc)
430
+ return []
431
+
432
+ return [
433
+ {"complexity": complexity, "tier": tier, "success": bool(success), "ts": ts}
434
+ for complexity, tier, success, ts in rows
435
+ ]
436
+
437
+
384
438
  def read_tier_failure_rate(
385
439
  complexity: str,
386
440
  tier: str,
@@ -168,7 +168,15 @@ class C3MCPServer:
168
168
  line = sys.stdin.readline()
169
169
  if not line:
170
170
  return {"action": "cancel"}
171
- payload = json.loads(line)
171
+ try:
172
+ payload = json.loads(line)
173
+ except json.JSONDecodeError as exc:
174
+ # Malformed JSON lines (e.g. partial writes) are logged and skipped.
175
+ print(
176
+ f"[c3 mcp_server] _elicit: invalid JSON skipped: {exc}",
177
+ file=sys.stderr,
178
+ )
179
+ continue
172
180
  if payload.get("id") == request_id and "result" in payload:
173
181
  return payload["result"]
174
182
  # Notifications can arrive while waiting for elicitation. Ignore them.
@@ -33,9 +33,13 @@ def load_questions(source: str | Path | dict[str, Any]) -> list[Question]:
33
33
  """Load one or more questions from a path, JSON string, or object."""
34
34
  if isinstance(source, dict):
35
35
  payload = source
36
+ elif isinstance(source, Path):
37
+ payload = json.loads(source.read_text(encoding="utf-8") if source.is_file() else str(source))
38
+ elif isinstance(source, str):
39
+ path = Path(source)
40
+ payload = json.loads(path.read_text(encoding="utf-8") if path.is_file() else source)
36
41
  else:
37
- text = Path(source).read_text(encoding="utf-8") if Path(str(source)).is_file() else str(source)
38
- payload = json.loads(text)
42
+ raise TypeError(f"source must be str, Path, or dict, got {type(source).__name__}")
39
43
 
40
44
  raw_questions = payload.get("questions") if isinstance(payload, dict) else None
41
45
  if raw_questions is None: