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.
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/consolidate_memory.py +25 -6
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/permission_handler.py +6 -2
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/post_tool.py +1 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/restore_session.py +16 -9
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/review_hint_inject.py +10 -2
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/select_tier.py +52 -15
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_utils.py +1 -2
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/stop.py +12 -2
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/CHANGELOG.md +33 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/PKG-INFO +1 -1
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/__init__.py +1 -1
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/adapters.py +6 -1
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_tier.py +6 -27
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/db.py +55 -1
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/mcp_server.py +9 -1
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/question.py +6 -2
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_consolidate_memory.py +168 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_planner_check.py +96 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_select_tier.py +96 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_ask.py +69 -0
- claude_code_conductor-2.6.1/tests/test_mcp_server_elicit.py +113 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/CLAUDE.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/architect.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/code-reviewer.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/developer.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/doc-writer.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/interviewer.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/planner.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/project-setup.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/security-reviewer.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/systematic-debugger.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/tester.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_developer.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_systematic-debugger.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/agents/wt_tester.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/docs/platform-adapters.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/docs/settings.json.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/pre_compact.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/pre_tool.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/record_review_decision.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/record_tier_outcome.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/schema.sql +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_start.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/session_stop.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/statusline.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/subagent_log.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/worktree_guard.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/memory/.gitkeep +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/permission_rules.json +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/rules/code-review-checklist.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/rules/promoted/index.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/rules/security-review-checklist.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/settings.json +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/code-review/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/codex-review/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/dev-workflow/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/develop/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/doc/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/extract-lib/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/init-session/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/mcp-config/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/parallel-agents/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/pattern-status/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/promote-pattern/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/report-timestamp/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/setup/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/start/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/skills/task-routing/SKILL.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/state/.gitkeep +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.gitignore +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/LICENSE +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/README.md +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/hatch_build.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/pyproject.toml +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/__main__.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/_excludes.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/_terminal.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_ask.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_doctor.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_init.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_list.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_plan.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/cli_update.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/paths.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/plan_validator.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/src/c3/platforms.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/__init__.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/conftest.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/__init__.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_permission_handler.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_post_tool.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_pre_tool.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_record_tier_outcome.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_restore_session.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_review_hint_inject.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_select_tier_escalation.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_start.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_stop.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_session_utils.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_similarity_boost.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_statusline.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_statusline_template_sync.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_subagent_log.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_sync_check.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/hooks/test_template_guard.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/__init__.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_session_backlog_reconciliation.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/skills/test_task_routing_skill.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_adapters.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_init.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_list.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_plan.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_cli_tier.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_docstring_consistency.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_excludes.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_paths.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_plan_validator.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_pre_compact.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_precompact_additional.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_precompact_toctou_fixes.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_session_utils_additional.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_statusline.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_stop_additional.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_stop_hook.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_stop_precompact_fixes.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_sync_template_stop.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_template_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/tests/test_worktree_guard.py +0 -0
{claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/consolidate_memory.py
RENAMED
|
@@ -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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
531
|
-
|
|
533
|
+
"""XML タグ境界突破を防ぐためタグ記号・引用符をエンティティに変換する。[SR-AI-001]
|
|
534
|
+
|
|
535
|
+
引用符 (" / ') もエスケープすることで、属性値混入による境界突破を防ぐ。
|
|
536
|
+
変換順: & を最初に変換してから他の文字を変換する(二重エスケープ防止)。
|
|
537
|
+
"""
|
|
538
|
+
return (
|
|
539
|
+
text
|
|
540
|
+
.replace("&", "&")
|
|
541
|
+
.replace("<", "<")
|
|
542
|
+
.replace(">", ">")
|
|
543
|
+
.replace('"', """)
|
|
544
|
+
.replace("'", "'")
|
|
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, "--
|
|
672
|
+
[claude_exe, "-p", prompt, "--tools", ""],
|
|
654
673
|
**run_kwargs,
|
|
655
674
|
)
|
|
656
675
|
except subprocess.TimeoutExpired:
|
{claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/permission_handler.py
RENAMED
|
@@ -47,13 +47,17 @@ def notify(message: str) -> None:
|
|
|
47
47
|
)
|
|
48
48
|
elif system == 'Windows':
|
|
49
49
|
import base64
|
|
50
|
-
|
|
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
|
|
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),
|
{claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/restore_session.py
RENAMED
|
@@ -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, 'うまくいったアプローチ')
|
{claude_code_conductor-2.6.0 → claude_code_conductor-2.6.1}/.claude/hooks/review_hint_inject.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
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:`
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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` に引用符エスケープ(`"` → `"`、`'` → `'`)を追加。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.
|
|
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
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|