claude-code-conductor 2.8.0__tar.gz → 2.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/permission_handler.py +5 -0
  2. claude_code_conductor-2.10.0/.claude/hooks/recall_inject.py +292 -0
  3. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/record_tier_outcome.py +35 -0
  4. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/restore_session.py +9 -0
  5. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/select_tier.py +13 -11
  6. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/session_utils.py +0 -1
  7. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/statusline.py +9 -17
  8. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/stop.py +4 -0
  9. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/subagent_log.py +2 -2
  10. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/worktree_guard.py +4 -0
  11. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/settings.json +6 -1
  12. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/dev-workflow/SKILL.md +65 -40
  13. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/init-session/SKILL.md +1 -4
  14. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/parallel-agents/SKILL.md +5 -0
  15. claude_code_conductor-2.10.0/.claude/skills/recall/SKILL.md +113 -0
  16. claude_code_conductor-2.10.0/.claude/skills/start/SKILL.md +136 -0
  17. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.gitignore +6 -0
  18. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/CHANGELOG.md +103 -1
  19. claude_code_conductor-2.10.0/LICENSES/chroma-hnswlib-LICENSE +184 -0
  20. claude_code_conductor-2.10.0/LICENSES/chroma-hnswlib-NOTICE +9 -0
  21. claude_code_conductor-2.10.0/LICENSES/fastembed-LICENSE +184 -0
  22. claude_code_conductor-2.10.0/LICENSES/fastembed-NOTICE +8 -0
  23. claude_code_conductor-2.10.0/LICENSES/onnxruntime-LICENSE +29 -0
  24. claude_code_conductor-2.10.0/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +142 -0
  25. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/PKG-INFO +39 -1
  26. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/README.md +36 -0
  27. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/hatch_build.py +2 -0
  28. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/pyproject.toml +4 -0
  29. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/__init__.py +1 -1
  30. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/_excludes.py +2 -0
  31. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/_terminal.py +1 -1
  32. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli.py +36 -2
  33. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli_plan.py +11 -7
  34. claude_code_conductor-2.10.0/src/c3/cli_recall.py +464 -0
  35. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli_tier.py +1 -1
  36. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/db.py +14 -7
  37. claude_code_conductor-2.10.0/src/c3/embedding.py +145 -0
  38. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/mcp_server.py +13 -0
  39. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/plan_validator.py +2 -1
  40. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/question.py +3 -0
  41. claude_code_conductor-2.10.0/src/c3/recall_chunker.py +161 -0
  42. claude_code_conductor-2.10.0/src/c3/recall_index.py +515 -0
  43. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_consolidate_memory.py +9 -12
  44. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_permission_handler.py +55 -0
  45. claude_code_conductor-2.10.0/tests/hooks/test_recall_inject.py +477 -0
  46. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_restore_session.py +9 -0
  47. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_session_stop.py +2 -1
  48. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_session_utils.py +9 -13
  49. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_statusline.py +13 -34
  50. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_statusline_template_sync.py +6 -0
  51. claude_code_conductor-2.10.0/tests/skills/_skill_helpers.py +137 -0
  52. claude_code_conductor-2.10.0/tests/skills/test_dev_workflow_no_task_type.py +151 -0
  53. claude_code_conductor-2.10.0/tests/skills/test_init_session_no_task_type.py +47 -0
  54. claude_code_conductor-2.10.0/tests/skills/test_recall_skill.py +98 -0
  55. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/skills/test_session_backlog_reconciliation.py +23 -35
  56. claude_code_conductor-2.10.0/tests/skills/test_start_skill_bugfix_flow.py +50 -0
  57. claude_code_conductor-2.10.0/tests/skills/test_start_skill_new_flow.py +128 -0
  58. claude_code_conductor-2.10.0/tests/skills/test_start_skill_security_audit_phase.py +77 -0
  59. claude_code_conductor-2.10.0/tests/test_cli_entry.py +73 -0
  60. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_cli_list.py +5 -5
  61. claude_code_conductor-2.10.0/tests/test_cli_recall.py +494 -0
  62. claude_code_conductor-2.10.0/tests/test_embedding.py +176 -0
  63. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_pre_compact.py +4 -6
  64. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_pre_tool_hook.py +14 -15
  65. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_precompact_toctou_fixes.py +4 -3
  66. claude_code_conductor-2.10.0/tests/test_recall_chunker.py +129 -0
  67. claude_code_conductor-2.10.0/tests/test_recall_index.py +592 -0
  68. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_session_utils_additional.py +14 -15
  69. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_statusline.py +27 -62
  70. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_stop_additional.py +9 -9
  71. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_stop_hook.py +8 -8
  72. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_stop_precompact_fixes.py +4 -3
  73. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_sync_template_stop.py +1 -1
  74. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_template_pre_tool_hook.py +6 -7
  75. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_worktree_guard.py +15 -2
  76. claude_code_conductor-2.8.0/.claude/skills/start/SKILL.md +0 -355
  77. claude_code_conductor-2.8.0/.claude/skills/task-routing/SKILL.md +0 -201
  78. claude_code_conductor-2.8.0/tests/skills/test_start_skill_bugfix_flow.py +0 -56
  79. claude_code_conductor-2.8.0/tests/skills/test_start_skill_security_audit_phase.py +0 -419
  80. claude_code_conductor-2.8.0/tests/skills/test_task_routing_skill.py +0 -77
  81. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/CLAUDE.md +0 -0
  82. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/architect.md +0 -0
  83. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/code-reviewer.md +0 -0
  84. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/developer.md +0 -0
  85. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/doc-writer.md +0 -0
  86. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/interviewer.md +0 -0
  87. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/planner.md +0 -0
  88. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/project-setup.md +0 -0
  89. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/security-reviewer.md +0 -0
  90. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/summarize-memory.md +0 -0
  91. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/systematic-debugger.md +0 -0
  92. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/tester.md +0 -0
  93. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/wt_developer.md +0 -0
  94. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/wt_systematic-debugger.md +0 -0
  95. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/agents/wt_tester.md +0 -0
  96. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/docs/platform-adapters.md +0 -0
  97. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/docs/settings.json.md +0 -0
  98. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/consolidate_memory.py +0 -0
  99. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/permission_handler_toast.py +0 -0
  100. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/post_tool.py +0 -0
  101. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/pre_compact.py +0 -0
  102. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/pre_tool.py +0 -0
  103. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/record_review_decision.py +0 -0
  104. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/review_hint_inject.py +0 -0
  105. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/schema.sql +0 -0
  106. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/session_start.py +0 -0
  107. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/hooks/session_stop.py +0 -0
  108. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/memory/.gitkeep +0 -0
  109. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/permission_rules.json +0 -0
  110. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/rules/code-review-checklist.md +0 -0
  111. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/rules/promoted/index.md +0 -0
  112. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/rules/security-review-checklist.md +0 -0
  113. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/code-review/SKILL.md +0 -0
  114. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/codex-review/SKILL.md +0 -0
  115. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/develop/SKILL.md +0 -0
  116. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/doc/SKILL.md +0 -0
  117. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/extract-lib/SKILL.md +0 -0
  118. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/mcp-config/SKILL.md +0 -0
  119. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/pattern-status/SKILL.md +0 -0
  120. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
  121. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
  122. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
  123. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/setup/SKILL.md +0 -0
  124. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/skills/summarize-memory/SKILL.md +0 -0
  125. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/.claude/state/.gitkeep +0 -0
  126. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/LICENSE +0 -0
  127. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/__main__.py +0 -0
  128. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/adapters.py +0 -0
  129. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli_ask.py +0 -0
  130. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli_doctor.py +0 -0
  131. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli_init.py +0 -0
  132. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli_list.py +0 -0
  133. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/cli_update.py +0 -0
  134. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/paths.py +0 -0
  135. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/src/c3/platforms.py +0 -0
  136. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/__init__.py +0 -0
  137. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/conftest.py +0 -0
  138. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/__init__.py +0 -0
  139. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_permission_handler_toast.py +0 -0
  140. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
  141. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_planner_check.py +0 -0
  142. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_post_tool.py +0 -0
  143. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_pre_tool.py +0 -0
  144. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_record_tier_outcome.py +0 -0
  145. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_review_hint_inject.py +0 -0
  146. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_select_tier.py +0 -0
  147. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_select_tier_escalation.py +0 -0
  148. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_session_start.py +0 -0
  149. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
  150. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_similarity_boost.py +0 -0
  151. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_subagent_log.py +0 -0
  152. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_sync_check.py +0 -0
  153. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/hooks/test_template_guard.py +0 -0
  154. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/skills/__init__.py +0 -0
  155. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_adapters.py +0 -0
  156. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_cli_ask.py +0 -0
  157. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_cli_init.py +0 -0
  158. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_cli_plan.py +0 -0
  159. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_cli_tier.py +0 -0
  160. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_docstring_consistency.py +0 -0
  161. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_excludes.py +0 -0
  162. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_mcp_server_elicit.py +0 -0
  163. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_paths.py +0 -0
  164. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_plan_validator.py +0 -0
  165. {claude_code_conductor-2.8.0 → claude_code_conductor-2.10.0}/tests/test_precompact_additional.py +0 -0
@@ -369,6 +369,11 @@ def main() -> None:
369
369
  rules = load_rules()
370
370
  description = describe_tool(tool_name, tool_input)
371
371
 
372
+ # AskUserQuestion は通知のみ。自動承認対象外として扱う。
373
+ if tool_name == 'AskUserQuestion':
374
+ notify(f'-> 質問: {description}')
375
+ return
376
+
372
377
  for pattern in rules.get('auto_allow', []):
373
378
  if matches_pattern(tool_name, tool_input, pattern):
374
379
  if rules.get('notify_on_auto', True):
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env python3
2
+ """UserPromptSubmit hook: inject semantically-similar past context (α design).
3
+
4
+ The hook runs ``c3 recall search`` against the local HNSW index and
5
+ returns the top hits as ``additionalContext`` for the parent Claude to
6
+ consider. The preface explicitly asks Claude to evaluate the relevance
7
+ of each hit and ignore unrelated ones — i.e. *AI judges, hook does not
8
+ filter aggressively*.
9
+
10
+ Skip conditions (all silent no-ops, exit 0):
11
+ - ``C3_RECALL_HOOK_DISABLE=1`` env var set
12
+ - Prompt shorter than :data:`_MIN_PROMPT_CHARS` (default 15)
13
+ - Prompt starts with ``/`` (slash command) or ``@`` (file mention)
14
+ - No ``.claude/state/recall_meta.json`` / ``recall.hnsw`` (index not built)
15
+ - ``c3.cli`` subprocess fails or times out
16
+ - Zero hits above ``--min-score``
17
+
18
+ Output protocol:
19
+ ``{"hookSpecificOutput": {"hookEventName": "UserPromptSubmit",
20
+ "additionalContext": "..."}}``
21
+
22
+ Performance: each invocation runs a fresh Python subprocess that loads
23
+ fastembed + onnxruntime + the MiniLM model. Cold-start is ~2-3 seconds,
24
+ warm cache ~1-2 seconds. Users can disable this hook entirely by setting
25
+ ``C3_RECALL_HOOK_DISABLE=1`` in the shell or in ``.env``.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import re
33
+ import subprocess
34
+ import sys
35
+ from pathlib import Path
36
+ from typing import Iterable
37
+
38
+ try:
39
+ sys.stdin.reconfigure(encoding="utf-8")
40
+ sys.stdout.reconfigure(encoding="utf-8")
41
+ sys.stderr.reconfigure(encoding="utf-8")
42
+ except AttributeError:
43
+ pass
44
+
45
+
46
+ # Minimum prompt length (chars) to bother running recall. Short messages
47
+ # like "yes" / "ok" / "go" rarely benefit from semantic recall and the
48
+ # subprocess overhead is wasteful.
49
+ _MIN_PROMPT_CHARS = 15
50
+
51
+ # SR-L-1: cap the prompt passed to the subprocess to avoid passing huge
52
+ # context windows through command-line arguments (OS arg-length limits and
53
+ # unnecessary embedding overhead).
54
+ _MAX_PROMPT_CHARS = 2000
55
+
56
+ _TOP_K = 3
57
+
58
+ # Slightly stricter than the CLI default (0.3) because the parent Claude
59
+ # pays a context cost for every injected line; surfacing weak matches
60
+ # isn't worth the noise.
61
+ _MIN_SCORE = 0.4
62
+
63
+ # Generous timeout to accommodate the fastembed cold-start the first
64
+ # time a Claude session warms up the cache.
65
+ _TIMEOUT_SEC = 8
66
+
67
+ _DISABLE_ENV_VAR = "C3_RECALL_HOOK_DISABLE"
68
+
69
+
70
+ # SR-M-1: strip control characters (except \t) and newlines from fields
71
+ # that are embedded inline into the additionalContext string. Unescaped
72
+ # newlines in path / chunk_label would allow a malicious file path or
73
+ # heading to inject extra lines (including header-like strings) into the
74
+ # context block seen by the parent LLM.
75
+ _CONTROL_RE = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]")
76
+
77
+
78
+ def _sanitize_field(s: str) -> str:
79
+ """Remove newlines and non-printable control chars from inline fields.
80
+
81
+ Any text following the first newline is stripped entirely (not just the
82
+ newline character) so that a malicious path like
83
+ ``normal/path\\nX-Injected: evil-header`` cannot smuggle arbitrary text
84
+ into the output after the newline is collapsed.
85
+ """
86
+ if not s:
87
+ return ""
88
+ # Take only text before the first newline.
89
+ first_line = s.split("\n")[0].split("\r")[0]
90
+ # Strip remaining control chars (but preserve tab for readability).
91
+ return _CONTROL_RE.sub("", first_line)
92
+
93
+
94
+ def should_skip_prompt(prompt: str) -> bool:
95
+ """Pure-logic check used by main() and the unit tests."""
96
+ if not prompt:
97
+ return True
98
+ stripped = prompt.strip()
99
+ if len(stripped) < _MIN_PROMPT_CHARS:
100
+ return True
101
+ if stripped.startswith("/"):
102
+ return True
103
+ if stripped.startswith("@"):
104
+ return True
105
+ return False
106
+
107
+
108
+ def format_additional_context(hits: Iterable[dict], *, stale: bool = False) -> str:
109
+ """Render hits into the ``additionalContext`` block.
110
+
111
+ The preface is the key design lever for α: it tells the parent Claude
112
+ that these are *candidates*, not *facts*, and asks it to discard
113
+ unrelated entries.
114
+
115
+ When ``stale`` is True, a directive is prepended that asks the parent
116
+ Claude to surface ``AskUserQuestion`` so the user can choose whether
117
+ to ``c3 recall rebuild``. The hook itself cannot invoke
118
+ ``AskUserQuestion`` — only the parent LLM can — so we route the
119
+ decision through context injection.
120
+ """
121
+ lines: list[str] = []
122
+ if stale:
123
+ lines.extend(
124
+ [
125
+ "[recall] ⚠️ インデックスが古い可能性があります(最新ソースの mtime > index mtime)。",
126
+ "次に進む前に、AskUserQuestion で以下の 3 択をユーザーに提示してください:",
127
+ " - 今すぐ rebuild する → Bash で `c3 recall rebuild` を実行(約 1〜2 分)",
128
+ " - 後で / 今は不要 → 検索結果は古いままという前提で続行",
129
+ " - 無視 → このまま現タスクを進める",
130
+ "ユーザーが同一セッション中にすでに「後で」「無視」と答えた場合は再度尋ねないでください。",
131
+ "",
132
+ ]
133
+ )
134
+ lines.append(
135
+ "[recall] 過去の類似情報の検索結果(参考のみ。**現タスクと無関係なら無視**してください):"
136
+ )
137
+ for i, hit in enumerate(hits, start=1):
138
+ score = hit.get("score")
139
+ # SR-M-1: sanitize inline fields to prevent newline / control-char
140
+ # injection from malicious file paths or headings.
141
+ path = _sanitize_field(hit.get("path") or "")
142
+ label = _sanitize_field(hit.get("chunk_label") or "")
143
+ snippet = (hit.get("snippet") or "").strip().replace("\n", " ")
144
+ if len(snippet) > 220:
145
+ snippet = snippet[:220] + "..."
146
+ score_str = f"{score:.2f}" if isinstance(score, (int, float)) else str(score)
147
+ lines.append(f" [{i}] score={score_str} {path} :: {label}")
148
+ if snippet:
149
+ lines.append(f" {snippet}")
150
+ return "\n".join(lines)
151
+
152
+
153
+ def find_repo_root() -> Path | None:
154
+ """Return the nearest ancestor containing ``.claude/`` (or None)."""
155
+ here = Path(os.getenv("CLAUDE_PROJECT_DIR") or Path.cwd()).resolve()
156
+ for candidate in [here, *here.parents]:
157
+ if (candidate / ".claude").is_dir():
158
+ return candidate
159
+ return None
160
+
161
+
162
+ def index_exists(repo_root: Path) -> bool:
163
+ meta = repo_root / ".claude" / "state" / "recall_meta.json"
164
+ index = repo_root / ".claude" / "state" / "recall.hnsw"
165
+ return meta.exists() and index.exists()
166
+
167
+
168
+ # Source directories scanned to decide if the recall index is older than
169
+ # at least one of its inputs. Mirrors :mod:`c3.recall_index.collect_sources`
170
+ # but kept local to the hook so it can run without importing the c3
171
+ # package (the hook may execute in environments where ``c3`` is not yet
172
+ # importable, e.g. immediately after ``c3 init``).
173
+ # CR-L-02: Keep in sync with c3.recall_index.collect_sources when adding
174
+ # or removing source kinds.
175
+ _STALE_SOURCE_GLOBS = (
176
+ (Path(".claude") / "memory" / "sessions", "*.tmp"),
177
+ (Path(".claude") / "agent-memory", "*.md"),
178
+ (Path(".claude") / "reports" / "archive", "*.md"),
179
+ )
180
+ _STALE_PATTERNS_JSON = Path(".claude") / "memory" / "patterns.json"
181
+
182
+
183
+ def index_is_stale(repo_root: Path) -> bool:
184
+ """Return True if any recall source is newer than the index file."""
185
+ index_path = repo_root / ".claude" / "state" / "recall.hnsw"
186
+ if not index_path.exists():
187
+ return False
188
+ index_mtime = index_path.stat().st_mtime
189
+ for rel_dir, pattern in _STALE_SOURCE_GLOBS:
190
+ absolute = repo_root / rel_dir
191
+ if not absolute.is_dir():
192
+ continue
193
+ for path in absolute.rglob(pattern):
194
+ # Cycle2-L-1 [SR-V-002]: skip symlinks to avoid reading mtime of
195
+ # files outside the C3 source tree (matches the analogous guard in
196
+ # c3.recall_index._collect_markdown_glob).
197
+ if not path.is_file() or path.name == ".gitkeep" or path.is_symlink():
198
+ continue
199
+ try:
200
+ if path.stat().st_mtime > index_mtime:
201
+ return True
202
+ except OSError:
203
+ continue
204
+ patterns_path = repo_root / _STALE_PATTERNS_JSON
205
+ if patterns_path.is_file():
206
+ try:
207
+ if patterns_path.stat().st_mtime > index_mtime:
208
+ return True
209
+ except OSError:
210
+ pass
211
+ return False
212
+
213
+
214
+ def run_recall(prompt: str, repo_root: Path) -> list[dict]:
215
+ """Invoke ``python -m c3.cli recall search`` and return ``hits`` list.
216
+
217
+ Any error path (subprocess failure, timeout, malformed JSON) returns
218
+ an empty list so the hook stays silent rather than surfacing errors
219
+ to the user mid-prompt.
220
+ """
221
+ # SR-L-1: truncate prompt to avoid OS arg-length limits and pass only
222
+ # the most relevant context to the embedding model.
223
+ prompt = prompt[:_MAX_PROMPT_CHARS]
224
+ try:
225
+ result = subprocess.run(
226
+ [
227
+ sys.executable,
228
+ "-m",
229
+ "c3.cli",
230
+ "recall",
231
+ "search",
232
+ prompt,
233
+ "--top",
234
+ str(_TOP_K),
235
+ "--min-score",
236
+ str(_MIN_SCORE),
237
+ "--json",
238
+ "--target",
239
+ str(repo_root),
240
+ ],
241
+ capture_output=True,
242
+ text=True,
243
+ encoding="utf-8",
244
+ timeout=_TIMEOUT_SEC,
245
+ )
246
+ except (subprocess.TimeoutExpired, OSError):
247
+ return []
248
+ if result.returncode != 0:
249
+ return []
250
+ try:
251
+ data = json.loads(result.stdout)
252
+ except (json.JSONDecodeError, ValueError):
253
+ return []
254
+ hits = data.get("hits") or []
255
+ return hits if isinstance(hits, list) else []
256
+
257
+
258
+ def main() -> int:
259
+ if os.environ.get(_DISABLE_ENV_VAR) == "1":
260
+ return 0
261
+
262
+ try:
263
+ payload = json.load(sys.stdin)
264
+ except (json.JSONDecodeError, ValueError):
265
+ return 0
266
+
267
+ prompt = payload.get("prompt", "")
268
+ if not isinstance(prompt, str) or should_skip_prompt(prompt):
269
+ return 0
270
+
271
+ repo_root = find_repo_root()
272
+ if repo_root is None or not index_exists(repo_root):
273
+ return 0
274
+
275
+ hits = run_recall(prompt, repo_root)
276
+ if not hits:
277
+ return 0
278
+
279
+ stale = index_is_stale(repo_root)
280
+
281
+ output = {
282
+ "hookSpecificOutput": {
283
+ "hookEventName": "UserPromptSubmit",
284
+ "additionalContext": format_additional_context(hits, stale=stale),
285
+ }
286
+ }
287
+ print(json.dumps(output, ensure_ascii=False))
288
+ return 0
289
+
290
+
291
+ if __name__ == "__main__":
292
+ sys.exit(main())
@@ -87,6 +87,40 @@ def _delete_tier_selection() -> None:
87
87
  )
88
88
 
89
89
 
90
+ # prompt-history.jsonl の上限サイズ(バイト)。超過時は末尾 _PROMPT_HISTORY_TRUNCATE_LINES 行
91
+ # だけを残してローテーションする。読み込み側 (select_tier._PROMPT_HISTORY_SCAN_LINES=1000) と
92
+ # 同じオーダーで保持し、ディスク消費を抑える [SR-V-001]。
93
+ _PROMPT_HISTORY_MAX_BYTES = 10 * 1024 * 1024 # 10 MB
94
+ _PROMPT_HISTORY_TRUNCATE_LINES = 2000
95
+
96
+
97
+ def _rotate_prompt_history_if_needed() -> None:
98
+ """prompt-history.jsonl が上限超過なら末尾 N 行を残して切り詰める。
99
+
100
+ 書き込み側のサイズ無制限成長を防ぐシンプルなローテーション。失敗時は警告のみ。
101
+ """
102
+ try:
103
+ size = os.path.getsize(PROMPT_HISTORY_PATH)
104
+ except OSError:
105
+ return
106
+ if size <= _PROMPT_HISTORY_MAX_BYTES:
107
+ return
108
+ try:
109
+ # 末尾 N 行のみ deque で保持して上書きする(ファイル全体は走査するが I/O のみ)
110
+ import collections as _c
111
+ with open(PROMPT_HISTORY_PATH, "r", encoding="utf-8") as f:
112
+ tail = list(_c.deque(f, maxlen=_PROMPT_HISTORY_TRUNCATE_LINES))
113
+ tmp_path = PROMPT_HISTORY_PATH + ".tmp"
114
+ with open(tmp_path, "w", encoding="utf-8") as f:
115
+ f.writelines(tail)
116
+ os.replace(tmp_path, PROMPT_HISTORY_PATH)
117
+ except OSError as exc:
118
+ print(
119
+ f"[record_tier_outcome] prompt-history rotate skipped: {exc}",
120
+ file=sys.stderr,
121
+ )
122
+
123
+
90
124
  def _append_prompt_history(selection: dict, success: bool) -> None:
91
125
  """Phase 2-C: prompt-history.jsonl に 1 行追記する。
92
126
 
@@ -108,6 +142,7 @@ def _append_prompt_history(selection: dict, success: bool) -> None:
108
142
  }
109
143
  try:
110
144
  os.makedirs(os.path.dirname(PROMPT_HISTORY_PATH), exist_ok=True)
145
+ _rotate_prompt_history_if_needed()
111
146
  with open(PROMPT_HISTORY_PATH, "a", encoding="utf-8") as f:
112
147
  f.write(json.dumps(record, ensure_ascii=False) + "\n")
113
148
  except OSError as exc:
@@ -41,6 +41,15 @@ def find_latest_session() -> str | None:
41
41
  return os.path.join(SESSIONS_DIR, max(files))
42
42
 
43
43
 
44
+ def extract_section(content: str, heading: str) -> str:
45
+ """``session_utils.extract_section`` への薄いラッパー(後方互換用)。
46
+
47
+ 過去にこのモジュール直下にあった ``extract_section`` を呼び出すテスト・スクリプトとの
48
+ 互換維持のため、モジュールレベルで公開する。実体は :mod:`session_utils` 側にある。
49
+ """
50
+ return _load_session_utils().extract_section(content, heading)
51
+
52
+
44
53
  def main():
45
54
  path = find_latest_session()
46
55
  if not path or not os.path.exists(path):
@@ -94,11 +94,15 @@ def _mask_secrets(text: str) -> str:
94
94
 
95
95
  キー名やプレフィックスは残し、値のみを置換することで
96
96
  「何が含まれていたか」は伝わらないようにする。
97
- PEM ブロックは開始タグ〜終了タグ全体を *** に置換する。
97
+ PEM ブロックは開始タグ + *** + 終了タグ に置換する。
98
98
  """
99
99
  result = text
100
100
  for pattern in _MASK_PATTERNS:
101
- result = pattern.sub(lambda m: m.group(1) + "***", result)
101
+ # group(2) があれば PEM ブロック (BEGIN...END)、なければプレフィックス系
102
+ result = pattern.sub(
103
+ lambda m: m.group(1) + "***" + (m.group(2) if m.lastindex and m.lastindex >= 2 else ""),
104
+ result,
105
+ )
102
106
  return result
103
107
 
104
108
  # prompt-history.jsonl の末尾から読む最大行数(パフォーマンス対策)
@@ -325,11 +329,11 @@ def write_tier_selection(
325
329
  record_tier_outcome.py がこの json を読んで α/β を更新する。
326
330
  既存ファイルは上書きされる(最新 1 件のみ保持)。
327
331
 
328
- tier-routing Phase 2-A: ``suggested_model`` も併せて書く。runner.py がこれを読んで
329
- PO 経由のサブエージェント起動時に ``claude --agents`` で動的に上書きする。
330
- tier 名と model の短縮名は同一とする。
332
+ ``suggested_model`` を併せて書く。tier 名と model の短縮名は同一とする。
333
+ PO 廃止前は runner.py が読んで ``claude --agents`` 用に使っていたが、v2.0.0 以降は
334
+ 記録目的のみ。将来再利用する余地のために維持する。)
331
335
 
332
- tier-routing Phase 2-B: ``escalated`` / ``escalation_reason`` を任意で含める。
336
+ ``escalated`` / ``escalation_reason`` を任意で含める。
333
337
  failure rate に基づく昇格が起きた場合のみ True / 文字列が入る。
334
338
  """
335
339
  os.makedirs(os.path.dirname(TIER_SELECTION_PATH), exist_ok=True)
@@ -337,7 +341,7 @@ def write_tier_selection(
337
341
  "complexity": complexity,
338
342
  "tier": tier,
339
343
  "mode": mode,
340
- # Phase 2-A: tier はそのまま claude --agents の model 短縮名として使える
344
+ # tier はそのまま claude --agents の model 短縮名として使える
341
345
  "suggested_model": tier,
342
346
  }
343
347
  if escalated:
@@ -383,10 +387,8 @@ def build_additional_context(
383
387
 
384
388
  return (
385
389
  f"[tier-routing 推奨] 複雑度: {complexity} / 推奨 Tier: {tier}({confidence})。"
386
- f"PO 経由のサブエージェント起動時はこの推奨が claude --agents JSON "
387
- f" 自動適用されます(Phase 2-A)。親 Claude の Agent ツール経由は依然"
388
- f" frontmatter 指定が優先されるため、コスト最適化したい場合は手動切替"
389
- f" してください。{suffix}"
390
+ f" Claude Agent ツール経由ではエージェント定義の frontmatter 指定が"
391
+ f" 優先されるため、コスト最適化したい場合は手動切替してください。{suffix}"
390
392
  )
391
393
 
392
394
 
@@ -20,7 +20,6 @@ def is_worktree(cwd: str) -> bool:
20
20
  def create_session_template(date_str: str) -> str:
21
21
  return (
22
22
  f"SESSION: {date_str}\n"
23
- f"TASK_TYPE: \n"
24
23
  f"AGENT: \n"
25
24
  f"DURATION: \n"
26
25
  f"\n"
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """Context gauge statusline script.
3
- Displays context usage + optional rate limit gauges (when plan provides rate_limits data).
2
+ """Statusline script for Claude Code.
3
+
4
+ Displays: [model display name] effort | ctx used X% | 5h lim X% | 7d lim X%
5
+
6
+ context_window_size (200K / 1M) は ctx used X% と情報重複のため表示しない。
7
+ gauge バー描画も省スペース優先で表示しない。
4
8
  """
5
9
 
6
10
  import json
@@ -35,14 +39,6 @@ def pct_color(pct: int) -> str:
35
39
  return GREEN
36
40
 
37
41
 
38
- def format_context_size(size: int) -> str:
39
- if size >= 900_000:
40
- return '1M'
41
- elif size >= 100_000:
42
- return '200K'
43
- return str(size)
44
-
45
-
46
42
  def format_reset_time(resets_at) -> str:
47
43
  if not resets_at:
48
44
  return ''
@@ -82,18 +78,13 @@ def render_output(raw: str) -> None:
82
78
  header: list[str] = []
83
79
  metrics: list[str] = []
84
80
 
85
- # [model display name] context_size effort — スペース区切り
81
+ # [model display name] effort — スペース区切り
82
+ # (context_window_size は ctx used X% と情報重複のため表示しない)
86
83
  model = data.get('model') or {}
87
84
  display_name = model.get('display_name', '')
88
85
  if display_name:
89
86
  header.append(f'[{display_name}]')
90
87
 
91
- # context window size: 200K / 1M
92
- ctx_window = data.get('context_window') or {}
93
- ctx_size = ctx_window.get('context_window_size')
94
- if ctx_size:
95
- header.append(format_context_size(int(ctx_size)))
96
-
97
88
  # effort level
98
89
  effort = data.get('effort') or {}
99
90
  effort_level = effort.get('level', '')
@@ -101,6 +92,7 @@ def render_output(raw: str) -> None:
101
92
  header.append(effort_level)
102
93
 
103
94
  # ctx usg %
95
+ ctx_window = data.get('context_window') or {}
104
96
  ctx_pct = round(ctx_window.get('used_percentage') or 0)
105
97
  metrics.append('ctx used ' + pct_color(ctx_pct) + str(ctx_pct) + '%' + RESET)
106
98
 
@@ -251,6 +251,10 @@ def update_patterns(date_str: str) -> None:
251
251
  continue
252
252
 
253
253
  registered = _parse_session_date(pattern['registered_date'])
254
+ if registered is None:
255
+ # registered_date が parse 不能ならパターンを保持して継続(クラッシュ回避)
256
+ active.append(pattern)
257
+ continue
254
258
  days_elapsed = (today - registered).days
255
259
 
256
260
  if days_elapsed >= EXPIRY_DAYS:
@@ -78,8 +78,8 @@ _SAFE_PAYLOAD_FIELDS = frozenset({
78
78
  # U+2028 (LINE SEPARATOR) / U+2029 (PARAGRAPH SEPARATOR) の定数 (sec-H-1)
79
79
  # ensure_ascii=False の json.dumps はこれらをエスケープしないため、
80
80
  # _append_log で明示的に \\u2028 / \\u2029 へ置換する。
81
- _U2028 = '
'
82
- _U2029 = '
'
81
+ _U2028 = '
' # LINE SEPARATOR
82
+ _U2029 = '
' # PARAGRAPH SEPARATOR
83
83
 
84
84
 
85
85
  def _now_iso() -> str:
@@ -5,6 +5,10 @@ PO_WORKTREE_GUARD=1 が設定されている場合のみ動作する。
5
5
  worktree 内で実装タスクを実行するワークフロー(parallel-agents skill が
6
6
  isolation:"worktree" 付きで起動する agent など)が事前にこの env を設定して有効化する。
7
7
  Write / Edit ツールの対象パスが CWD(worktree ルート)外であればブロックする。
8
+
9
+ NOTE [SR-V-002]: env 未設定時にガードが無効化されるリスクは parallel-agents/SKILL.md
10
+ で `PO_WORKTREE_GUARD=1` 設定を必須化することで運用上対処する。CWD parts チェック
11
+ 単独で自動有効化する設計変更は次回 major bump で検討予定。
8
12
  """
9
13
 
10
14
  import json
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "statusLine": {
65
65
  "type": "command",
66
- "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/statusline.py\""
66
+ "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/statusline.py\""
67
67
  },
68
68
  "hooks": {
69
69
  "PreToolUse": [
@@ -140,6 +140,11 @@
140
140
  "type": "command",
141
141
  "command": "python",
142
142
  "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/select_tier.py"]
143
+ },
144
+ {
145
+ "type": "command",
146
+ "command": "python",
147
+ "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/recall_inject.py"]
143
148
  }
144
149
  ]
145
150
  }