aiwcli 0.9.8 → 0.10.1

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 (115) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +3 -3
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  17. package/dist/templates/_shared/hooks/context_monitor.py +104 -247
  18. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  19. package/dist/templates/_shared/hooks/pre_compact.py +47 -32
  20. package/dist/templates/_shared/hooks/session_end.py +114 -60
  21. package/dist/templates/_shared/hooks/session_start.py +127 -81
  22. package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
  23. package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
  24. package/dist/templates/_shared/hooks/user_prompt_submit.py +47 -81
  25. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/hook_utils.py +207 -11
  32. package/dist/templates/_shared/lib/base/inference.py +121 -0
  33. package/dist/templates/_shared/lib/base/logger.py +291 -0
  34. package/dist/templates/_shared/lib/base/utils.py +42 -9
  35. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  36. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/context_formatter.py +317 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +508 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +653 -0
  47. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  48. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  49. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  51. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  52. package/dist/templates/_shared/lib/templates/README.md +5 -13
  53. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  54. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  56. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  57. package/dist/templates/_shared/lib/templates/plan_context.py +22 -37
  58. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/scripts/save_handoff.py +31 -19
  61. package/dist/templates/_shared/scripts/status_line.py +701 -0
  62. package/dist/templates/_shared/workflows/handoff.md +9 -3
  63. package/dist/templates/cc-native/.claude/settings.json +37 -14
  64. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  65. package/dist/templates/cc-native/MIGRATION.md +1 -1
  66. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +54 -21
  68. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  71. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +76 -89
  75. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
  76. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  78. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  79. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  86. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  87. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  96. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  97. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  98. package/oclif.manifest.json +1 -1
  99. package/package.json +1 -1
  100. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  101. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  102. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  103. package/dist/templates/_shared/lib/context/cache.py +0 -444
  104. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  105. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  106. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  107. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  108. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  109. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  110. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  111. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  115. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -45,369 +45,226 @@ Hook JSON excludes system prompt, tools, MCP tokens. We add a baseline
45
45
  to compensate (~22.6k tokens typical). See:
46
46
  https://github.com/anthropics/claude-code/issues/13783
47
47
  """
48
- import json
49
48
  import sys
50
49
  from pathlib import Path
51
- from typing import Optional, Tuple
50
+ from typing import Optional
52
51
 
53
52
  # Add parent directories to path for imports
54
53
  SCRIPT_DIR = Path(__file__).resolve().parent
55
54
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
56
55
  sys.path.insert(0, str(SHARED_LIB.parent))
57
56
 
58
- from lib.base.hook_utils import load_hook_input
59
- from lib.base.utils import eprint, project_dir
60
- from lib.context.context_manager import (
57
+ from lib.base.hook_utils import emit_context, load_hook_input, get_context_percent_remaining, log_debug, log_info, log_warn, log_error, log_diagnostic
58
+ from lib.base.utils import now_iso, project_dir
59
+ from lib.context.context_store import (
61
60
  get_all_contexts,
62
61
  get_context_by_session_id,
63
- update_plan_status,
62
+ maybe_activate,
63
+ save_state,
64
64
  )
65
- from lib.context.auto_state import save_auto_state
66
- from lib.context.event_log import EVENT_AUTO_STATE_SAVED, append_event
67
65
 
68
66
  # Module-level flag: only save auto-state once per process lifetime
69
- # Since hooks are separate processes per invocation, we use a file marker instead
70
67
  _PROGRESSIVE_SAVE_MARKER = ".progressive-save-done"
71
68
 
72
69
  # Configuration
73
70
  SAVE_STATE_THRESHOLD = 60 # Silently save auto-state at 60% remaining
74
- LOW_CONTEXT_THRESHOLD = 40 # Warn when below 40% remaining
75
- CRITICAL_CONTEXT_THRESHOLD = 25 # Urgent warning below 25%
76
-
77
- # Context baseline: preloaded tokens not visible to hooks (~22.6k typical)
78
- # This includes system prompt, tools, MCP tokens that aren't in hook data
79
- CONTEXT_BASELINE = 22_600
80
-
81
- # Default context window size (used when not provided in hook input)
82
- DEFAULT_CONTEXT_WINDOW = 200_000
83
-
84
-
85
- def get_context_tokens_from_hook(hook_input: dict) -> Tuple[Optional[int], Optional[int]]:
86
- """
87
- Extract actual token counts from Claude Code hook input.
88
-
89
- Claude Code provides context_window data with actual token counts:
90
- - cache_read_input_tokens: Tokens read from cache
91
- - input_tokens: New input tokens
92
- - cache_creation_input_tokens: Tokens written to cache
93
- - output_tokens: Model output tokens
94
-
95
- Args:
96
- hook_input: Hook input data from Claude Code
97
-
98
- Returns:
99
- Tuple of (tokens_used, max_tokens) or (None, None) if not available
100
- """
101
- context_window = hook_input.get("context_window")
102
- if not context_window:
103
- return None, None
104
-
105
- current_usage = context_window.get("current_usage")
106
- if not current_usage:
107
- return None, None
108
-
109
- # Sum all token types
110
- cache_read = current_usage.get("cache_read_input_tokens", 0) or 0
111
- input_tokens = current_usage.get("input_tokens", 0) or 0
112
- cache_creation = current_usage.get("cache_creation_input_tokens", 0) or 0
113
- output_tokens = current_usage.get("output_tokens", 0) or 0
114
-
115
- content_tokens = cache_read + input_tokens + cache_creation + output_tokens
116
-
117
- # Add baseline for system prompt, tools, MCP tokens not in hook data
118
- tokens_used = content_tokens + CONTEXT_BASELINE
119
-
120
- # Get max context window from hook input
121
- max_tokens = context_window.get("context_window_size") or DEFAULT_CONTEXT_WINDOW
122
-
123
- return tokens_used, max_tokens
71
+ HANDOFF_SUGGEST_THRESHOLD = 30 # Gentle nudge at 30% remaining (70% used)
72
+ HANDOFF_PREPARE_THRESHOLD = 20 # Stronger warning at 20% remaining (80% used)
73
+ CRITICAL_CONTEXT_THRESHOLD = 10 # Urgent warning at 10% remaining (90% used)
124
74
 
125
75
 
126
76
  def get_current_context_id(project_root: Path = None) -> Optional[str]:
127
- """
128
- Determine the current active context.
129
-
130
- Falls back to most recently active context.
131
-
132
- Returns:
133
- Context ID or None if no active context
134
- """
77
+ """Determine the current active context (most recently active)."""
135
78
  contexts = get_all_contexts(status="active", project_root=project_root)
136
79
  if contexts:
137
- return contexts[0].id # Sorted by last_active desc
80
+ return contexts[0].id
138
81
  return None
139
82
 
140
83
 
141
84
  def get_context_warning(
142
85
  percent_remaining: int,
143
- tokens_used: int,
144
- max_tokens: int,
86
+ tokens_used: Optional[int],
87
+ max_tokens: Optional[int],
145
88
  context_id: Optional[str],
146
89
  tool_name: str
147
90
  ) -> str:
148
- """
149
- Generate appropriate warning based on context level.
150
-
151
- Args:
152
- percent_remaining: Percentage of context remaining
153
- tokens_used: Estimated tokens used
154
- max_tokens: Maximum context window
155
- context_id: Current context ID (if any)
156
- tool_name: Tool that triggered this check
91
+ """Generate appropriate warning based on context level."""
92
+ if tokens_used is not None and max_tokens is not None:
93
+ tokens_used_k = tokens_used // 1000
94
+ max_tokens_k = max_tokens // 1000
95
+ usage_line = f"**Estimated usage**: ~{tokens_used_k}k / {max_tokens_k}k tokens"
96
+ else:
97
+ usage_line = f"**Estimated usage**: ~{percent_remaining}% remaining"
157
98
 
158
- Returns:
159
- System reminder markdown
160
- """
161
- # Format token counts
162
- tokens_used_k = tokens_used // 1000
163
- max_tokens_k = max_tokens // 1000
99
+ context_line = f"\nContext ID: `{context_id}`" if context_id else ""
164
100
 
165
101
  if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD:
166
- urgency = "CRITICAL"
167
- instruction = "You MUST wrap up immediately and create a handoff document."
168
- else:
169
- urgency = "LOW"
170
- instruction = "Please wrap up your current task and prepare for handoff."
102
+ return f"""<system-reminder>
103
+ ## CRITICAL CONTEXT WARNING ({percent_remaining}% remaining)
104
+
105
+ {usage_line}
106
+ **Triggered by**: {tool_name} tool completion
171
107
 
172
- context_info = ""
173
- if context_id:
174
- context_info = f"""
175
- To create a handoff document, use the /handoff command or describe:
176
- - What you were working on
177
- - What's completed
178
- - What still needs to be done
179
- - Any important decisions or context
108
+ **CRITICAL: Run `/handoff` now before context is compacted.**
109
+ {context_line}
180
110
 
181
- Context ID: `{context_id}`"""
111
+ You are about to lose context. Stop all other work and run `/handoff` immediately.
112
+ </system-reminder>"""
182
113
 
183
- return f"""<system-reminder>
184
- ## {urgency} CONTEXT WARNING ({percent_remaining}% remaining)
114
+ elif percent_remaining <= HANDOFF_PREPARE_THRESHOLD:
115
+ return f"""<system-reminder>
116
+ ## LOW CONTEXT WARNING ({percent_remaining}% remaining)
185
117
 
186
- **Estimated usage**: ~{tokens_used_k}k / {max_tokens_k}k tokens
118
+ {usage_line}
187
119
  **Triggered by**: {tool_name} tool completion
188
120
 
189
- {instruction}
190
- {context_info}
121
+ **Context is getting low. Please finish your current task and run `/handoff`.**
122
+ {context_line}
191
123
 
192
124
  **Actions:**
193
125
  1. Complete your current atomic task (if 1-2 steps away)
194
126
  2. Do NOT start new multi-step work
195
- 3. Create a handoff document summarizing progress
196
- 4. Ask user: "Context is getting low. I've summarized my progress. Should we continue in a new session?"
127
+ 3. Run `/handoff` to generate a handoff document
197
128
  </system-reminder>"""
198
129
 
130
+ else:
131
+ return f"""<system-reminder>
132
+ ## CONTEXT NOTICE ({percent_remaining}% remaining)
199
133
 
200
- def check_and_transition_mode(hook_input: dict) -> None:
201
- """
202
- Check if context needs to transition to implementing mode.
134
+ {usage_line}
135
+ **Triggered by**: {tool_name} tool completion
203
136
 
204
- This handles two cases:
205
- 1. Plan was approved (pending_implementation) and implementation tools are used
206
- 2. Plan was in planning mode but permission_mode is no longer "plan"
207
- (e.g., after /clear which clears permissions and pastes the plan)
137
+ **Consider preparing a handoff soon. When ready, run `/handoff` to generate a handoff document.**
138
+ {context_line}
208
139
 
209
- If we're seeing tool usage (Edit, Write, Bash) and either:
210
- - Context is in "pending_implementation", OR
211
- - Context is in "planning" and permission_mode is not "plan"
212
- We transition to "implementing".
140
+ Continue your current work, but avoid starting large new tasks.
141
+ </system-reminder>"""
213
142
 
214
- Args:
215
- hook_input: Hook input data from Claude Code
216
- """
217
- # Only transition on tools that indicate implementation work
218
- implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
219
- tool_name = hook_input.get("tool_name", "")
220
143
 
221
- if tool_name not in implementation_tools:
222
- return
144
+ def check_and_transition_mode(hook_input: dict) -> None:
145
+ """
146
+ Check if context mode needs to transition based on tool usage.
223
147
 
148
+ Handles:
149
+ - has_plan + implementation tool -> active (started implementing)
150
+ - idle + implementation tool -> active
151
+ """
224
152
  project_root = project_dir(hook_input)
225
153
  session_id = hook_input.get("session_id")
226
154
 
227
155
  if not session_id:
228
156
  return
229
157
 
230
- # Get context for this session
231
- context = get_context_by_session_id(session_id, project_root)
232
- if not context:
158
+ state = get_context_by_session_id(session_id, project_root)
159
+ if not state:
233
160
  return
234
161
 
235
- if not context.in_flight:
162
+ # Implementation transitions only trigger on implementation tools
163
+ implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
164
+ tool_name = hook_input.get("tool_name", "")
165
+
166
+ if tool_name not in implementation_tools:
236
167
  return
237
168
 
238
- current_mode = context.in_flight.mode
239
169
  permission_mode = hook_input.get("permission_mode", "default")
240
-
241
- # Transition from pending_implementation to implementing
242
- if current_mode == "pending_implementation":
243
- eprint(f"[context_monitor] Transitioning {context.id} from pending_implementation to implementing")
244
- update_plan_status(context.id, "implementing", project_root=project_root)
245
-
246
- # Transition from planning to implementing if permission_mode is not "plan"
247
- elif current_mode == "planning" and permission_mode != "plan":
248
- eprint(f"[context_monitor] Transitioning {context.id} from planning to implementing (permission_mode={permission_mode})")
249
- update_plan_status(context.id, "implementing", project_root=project_root)
170
+ maybe_activate(state.id, permission_mode, project_root=project_root, caller="context_monitor")
250
171
 
251
172
 
252
173
  def _try_progressive_save(hook_input: dict, percent_remaining: int) -> None:
253
- """
254
- Silently save auto-state at SAVE_STATE_THRESHOLD (60%).
255
-
256
- Uses a marker file in the context folder to ensure this fires only
257
- once per session. The marker is the session_id written to a file.
258
-
259
- Args:
260
- hook_input: Hook input data from Claude Code
261
- percent_remaining: Current context percentage remaining
262
- """
174
+ """Silently save state at SAVE_STATE_THRESHOLD (60%)."""
263
175
  try:
264
176
  session_id = hook_input.get("session_id", "")
265
177
  if not session_id:
266
178
  return
267
179
 
268
180
  project_root = project_dir(hook_input)
269
- context = get_context_by_session_id(session_id, project_root)
270
- if not context:
181
+ state = get_context_by_session_id(session_id, project_root)
182
+ if not state:
271
183
  return
272
184
 
273
185
  from lib.base.constants import get_context_dir
274
- marker_path = get_context_dir(context.id, project_root) / _PROGRESSIVE_SAVE_MARKER
275
- # Check if already saved for this session
186
+ marker_path = get_context_dir(state.id, project_root) / _PROGRESSIVE_SAVE_MARKER
276
187
  if marker_path.exists():
277
188
  try:
278
189
  saved_session = marker_path.read_text(encoding="utf-8").strip()
279
190
  if saved_session == session_id:
280
- return # Already saved this session
191
+ return
281
192
  except OSError:
282
193
  pass
283
194
 
284
- eprint(f"[context_monitor] Progressive save at {percent_remaining}% remaining")
285
-
286
- in_flight_mode = context.in_flight.mode if context.in_flight else "none"
287
- plan_path = context.in_flight.artifact_path if context.in_flight else None
288
- handoff_path = context.in_flight.handoff_path if context.in_flight else None
289
- transcript_path = hook_input.get("transcript_path")
290
-
291
- saved = save_auto_state(
292
- context_id=context.id,
293
- session_id=session_id,
294
- save_reason="progressive",
295
- project_root=project_root,
296
- in_flight_mode=in_flight_mode,
297
- plan_path=plan_path,
298
- handoff_path=handoff_path,
299
- transcript_path=transcript_path,
300
- )
301
-
302
- if saved:
303
- append_event(
304
- context.id, EVENT_AUTO_STATE_SAVED, project_root,
305
- session_id=session_id, save_reason="progressive",
306
- )
307
- # Write marker so we don't save again this session
308
- try:
309
- marker_path.write_text(session_id, encoding="utf-8")
310
- except OSError:
311
- pass
195
+ log_info("context_monitor", f"Progressive save at {percent_remaining}% remaining")
312
196
 
313
- except Exception as e:
314
- eprint(f"[context_monitor] Progressive save error (non-fatal): {e}")
197
+ # Just update last_active and save state
198
+ state.last_active = now_iso()
199
+ save_state(state, project_root)
315
200
 
201
+ try:
202
+ marker_path.write_text(session_id, encoding="utf-8")
203
+ except OSError:
204
+ pass
316
205
 
317
- def check_context_level(hook_input: dict) -> Optional[str]:
318
- """
319
- Check context level and return warning if low.
320
-
321
- Optimized for fail-fast: checks cheap conditions first before any file I/O.
206
+ except Exception as e:
207
+ log_warn("context_monitor", f"Progressive save error (non-fatal): {e}")
322
208
 
323
- Args:
324
- hook_input: Hook input data from Claude Code
325
209
 
326
- Returns:
327
- System reminder string if context is low, None otherwise
328
- """
329
- # === FAST PATH: No I/O, just dict lookups and math ===
210
+ def check_context_level(hook_input: dict) -> Optional[str]:
211
+ """Check context level and return warning if low."""
212
+ tool_name = hook_input.get("tool_name", "Unknown")
213
+ percent_remaining, tokens_used, max_tokens = get_context_percent_remaining(hook_input)
330
214
 
331
- # 1. Try to get context_window data (fast dict access)
332
- tokens_used, max_tokens = get_context_tokens_from_hook(hook_input)
215
+ log_diagnostic("context_monitor", "receive", f"tool={tool_name}, pct_remaining={percent_remaining}",
216
+ inputs={"tool_name": tool_name, "percent_remaining": percent_remaining,
217
+ "tokens_used": tokens_used, "max_tokens": max_tokens})
333
218
 
334
- # 2. If no context_window data, exit immediately - can't monitor accurately
335
- if tokens_used is None or max_tokens is None:
336
- # Only log if we want to debug
337
- # eprint("[context_monitor] context_window data unavailable")
219
+ if percent_remaining is None:
338
220
  return None
339
221
 
340
- # 3. Calculate percentage (fast math)
341
- remaining = max_tokens - tokens_used
342
- percent_remaining = max(0, min(100, int((remaining / max_tokens) * 100)))
343
-
344
- # 4. Most common case: context is fine, exit early
345
222
  if percent_remaining > SAVE_STATE_THRESHOLD:
346
223
  return None
347
224
 
348
- # === PROGRESSIVE SAVE: At 60% remaining, silently save auto-state ===
349
- if percent_remaining > LOW_CONTEXT_THRESHOLD:
350
- # Only save once per session (check marker file)
225
+ if percent_remaining > HANDOFF_SUGGEST_THRESHOLD:
351
226
  _try_progressive_save(hook_input, percent_remaining)
352
227
  return None
353
228
 
354
- # === SLOW PATH: Only reached when context is low (rare) ===
355
-
356
- # Log since we're in warning territory
357
- eprint(f"[context_monitor] Context: {percent_remaining}% remaining "
358
- f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
229
+ if tokens_used is not None and max_tokens is not None:
230
+ log_info("context_monitor", f"Context: {percent_remaining}% remaining "
231
+ f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
232
+ else:
233
+ log_info("context_monitor", f"Context: ~{percent_remaining}% remaining (from context.json)")
359
234
 
360
- # Get current context for handoff info (file I/O)
361
235
  project_root = project_dir(hook_input)
362
236
  context_id = get_current_context_id(project_root)
363
237
 
364
- tool_name = hook_input.get("tool_name", "Unknown")
238
+ threshold = ("critical" if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD
239
+ else "prepare" if percent_remaining <= HANDOFF_PREPARE_THRESHOLD
240
+ else "suggest")
241
+ log_diagnostic("context_monitor", "decide", f"Threshold={threshold} at {percent_remaining}%",
242
+ decision=threshold, reasoning=f"{percent_remaining}% remaining",
243
+ inputs={"context_id": context_id, "percent_remaining": percent_remaining})
365
244
 
366
- return get_context_warning(
367
- percent_remaining,
368
- tokens_used,
369
- max_tokens,
370
- context_id,
371
- tool_name
372
- )
245
+ return get_context_warning(percent_remaining, tokens_used, max_tokens, context_id, tool_name)
373
246
 
374
247
 
375
248
  def main():
376
- """
377
- Main entry point for PostToolUse hook.
378
-
379
- Reads hook input from stdin, checks for mode transitions,
380
- and prints system reminder if context is low.
381
- """
249
+ """Main entry point for PostToolUse hook."""
382
250
  try:
383
- # Read hook input using shared utility
384
251
  hook_input = load_hook_input()
385
-
386
252
  if not hook_input:
387
253
  return
388
254
 
389
- # Always check for mode transitions on implementation tools
390
- # This handles the case where /clear pastes the plan with non-plan permission mode
391
255
  check_and_transition_mode(hook_input)
392
256
 
393
- # Check context level
394
257
  warning = check_context_level(hook_input)
395
-
396
258
  if warning:
397
- # Output JSON with additionalContext so Claude sees the warning
398
- # Plain stdout from PostToolUse only goes to verbose mode, not Claude's context
399
- output = {
400
- "hookSpecificOutput": {
401
- "additionalContext": warning
402
- }
403
- }
404
- print(json.dumps(output))
259
+ emit_context(warning)
405
260
 
406
261
  except Exception as e:
407
- eprint(f"[context_monitor] ERROR: {e}")
408
262
  import traceback
409
- eprint(traceback.format_exc())
263
+ tb = traceback.format_exc()
264
+ from lib.base.hook_utils import log_hook_error
265
+ log_hook_error("context_monitor", e, "PostToolUse", traceback_str=tb)
410
266
 
411
267
 
412
268
  if __name__ == "__main__":
413
- main()
269
+ from lib.base.hook_utils import run_hook
270
+ run_hook(main, "context_monitor")
@@ -28,17 +28,17 @@ SCRIPT_DIR = Path(__file__).resolve().parent
28
28
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
29
29
  sys.path.insert(0, str(SHARED_LIB.parent))
30
30
 
31
- from lib.base.hook_utils import load_hook_input
32
- from lib.base.utils import eprint, project_dir
31
+ from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_error
32
+ from lib.base.utils import project_dir
33
33
  from lib.base.constants import (
34
34
  get_context_plans_dir,
35
35
  get_context_handoffs_dir,
36
36
  get_context_reviews_dir,
37
37
  get_context_file_path,
38
38
  )
39
- from lib.context.context_manager import (
39
+ from lib.context.context_store import (
40
40
  get_context_by_session_id,
41
- get_all_in_flight_contexts,
41
+ get_all_contexts,
42
42
  get_context,
43
43
  )
44
44
 
@@ -66,7 +66,7 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
66
66
  context_file = get_context_file_path(context_id, project_root)
67
67
  if context_file.exists():
68
68
  files.append(str(context_file))
69
- eprint(f"[file-suggestion] Found context file for {context_id}")
69
+ log_debug("file-suggestion", f"Found context file for {context_id}")
70
70
 
71
71
  # Get plans directory
72
72
  plans_dir = get_context_plans_dir(context_id, project_root)
@@ -75,7 +75,7 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
75
75
  # Sort by modification time, most recent first
76
76
  plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
77
77
  files.extend([str(p) for p in plan_files])
78
- eprint(f"[file-suggestion] Found {len(plan_files)} plans in {context_id}")
78
+ log_debug("file-suggestion", f"Found {len(plan_files)} plans in {context_id}")
79
79
 
80
80
  # Get handoffs - prefer folder-based (index.md in subdirectories), fall back to legacy
81
81
  handoffs_dir = get_context_handoffs_dir(context_id, project_root)
@@ -92,14 +92,14 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
92
92
  index_file = handoff_folders[0] / "index.md"
93
93
  if index_file.exists():
94
94
  files.append(str(index_file))
95
- eprint(f"[file-suggestion] Found handoff folder: {handoff_folders[0].name}")
95
+ log_debug("file-suggestion", f"Found handoff folder: {handoff_folders[0].name}")
96
96
  else:
97
97
  # Legacy support: flat .md files directly in handoffs/
98
98
  legacy_handoffs = [f for f in handoffs_dir.glob("*.md") if f.is_file()]
99
99
  legacy_handoffs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
100
100
  if legacy_handoffs:
101
101
  files.append(str(legacy_handoffs[0])) # Only most recent legacy
102
- eprint(f"[file-suggestion] Found {len(legacy_handoffs)} legacy handoffs in {context_id}")
102
+ log_debug("file-suggestion", f"Found {len(legacy_handoffs)} legacy handoffs in {context_id}")
103
103
 
104
104
  # Get reviews - prefer folder-based (index.md in subdirectories), fall back to legacy
105
105
  reviews_dir = get_context_reviews_dir(context_id, project_root) / "cc-native"
@@ -116,13 +116,13 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
116
116
  index_file = review_folders[0] / "index.md"
117
117
  if index_file.exists():
118
118
  files.append(str(index_file))
119
- eprint(f"[file-suggestion] Found review folder: {review_folders[0].name}")
119
+ log_debug("file-suggestion", f"Found review folder: {review_folders[0].name}")
120
120
  else:
121
121
  # Legacy support: flat review.md directly in cc-native/
122
122
  legacy_review = reviews_dir / "review.md"
123
123
  if legacy_review.exists():
124
124
  files.append(str(legacy_review))
125
- eprint(f"[file-suggestion] Found legacy review.md in {context_id}")
125
+ log_debug("file-suggestion", f"Found legacy review.md in {context_id}")
126
126
 
127
127
  return files
128
128
 
@@ -147,16 +147,17 @@ def get_active_context_id(session_id: str, project_root: Path) -> Optional[str]:
147
147
  if session_id and session_id != "unknown":
148
148
  context = get_context_by_session_id(session_id, project_root)
149
149
  if context:
150
- eprint(f"[file-suggestion] Found context by session: {context.id}")
150
+ log_debug("file-suggestion", f"Found context by session: {context.id}")
151
151
  return context.id
152
152
 
153
- # Fall back to single in-flight context
154
- in_flight = get_all_in_flight_contexts(project_root)
155
- if len(in_flight) == 1:
156
- eprint(f"[file-suggestion] Using single in-flight context: {in_flight[0].id}")
157
- return in_flight[0].id
153
+ # Fall back to single active (non-idle) context
154
+ active = [c for c in get_all_contexts(status="active", project_root=project_root)
155
+ if c.mode != "idle"]
156
+ if len(active) == 1:
157
+ log_debug("file-suggestion", f"Using single active context: {active[0].id}")
158
+ return active[0].id
158
159
 
159
- eprint(f"[file-suggestion] No unique context found (in-flight: {len(in_flight)})")
160
+ log_debug("file-suggestion", f"No unique context found (active: {len(active)})")
160
161
  return None
161
162
 
162
163
 
@@ -179,7 +180,7 @@ def main():
179
180
  project_root = project_dir(hook_input)
180
181
  session_id = hook_input.get("session_id", "unknown")
181
182
 
182
- eprint(f"[file-suggestion] Session: {session_id[:8]}..., Project: {project_root}")
183
+ log_debug("file-suggestion", f"Session: {session_id[:8]}..., Project: {project_root}")
183
184
 
184
185
  # Determine active context
185
186
  context_id = get_active_context_id(session_id, project_root)
@@ -194,19 +195,21 @@ def main():
194
195
  # Limit suggestions to prevent overwhelming the context
195
196
  MAX_SUGGESTIONS = 10
196
197
  if len(suggestions) > MAX_SUGGESTIONS:
197
- eprint(f"[file-suggestion] Limiting suggestions to {MAX_SUGGESTIONS} (was {len(suggestions)})")
198
+ log_debug("file-suggestion", f"Limiting suggestions to {MAX_SUGGESTIONS} (was {len(suggestions)})")
198
199
  suggestions = suggestions[:MAX_SUGGESTIONS]
199
200
 
200
201
  # Output suggestions as JSON array
201
- eprint(f"[file-suggestion] Suggesting {len(suggestions)} files")
202
+ log_info("file-suggestion", f"Suggesting {len(suggestions)} files")
202
203
  print(json.dumps(suggestions))
203
204
 
204
205
  except Exception as e:
205
- eprint(f"[file-suggestion] ERROR: {e}")
206
206
  import traceback
207
- eprint(traceback.format_exc())
207
+ tb = traceback.format_exc()
208
+ from lib.base.hook_utils import log_hook_error
209
+ log_hook_error("file-suggestion", e, "SessionStart", traceback_str=tb)
208
210
  print("[]")
209
211
 
210
212
 
211
213
  if __name__ == "__main__":
212
- main()
214
+ from lib.base.hook_utils import run_hook
215
+ run_hook(main, "file_suggestion")