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.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +3 -3
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +104 -247
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +47 -32
- package/dist/templates/_shared/hooks/session_end.py +114 -60
- package/dist/templates/_shared/hooks/session_start.py +127 -81
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
- package/dist/templates/_shared/hooks/user_prompt_submit.py +47 -81
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +207 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +42 -9
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +317 -0
- package/dist/templates/_shared/lib/context/context_selector.py +508 -0
- package/dist/templates/_shared/lib/context/context_store.py +653 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +22 -37
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +31 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +37 -14
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +54 -21
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +76 -89
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
- package/dist/templates/_shared/lib/context/auto_state.py +0 -167
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
- package/dist/templates/_shared/lib/context/discovery.py +0 -554
- package/dist/templates/_shared/lib/context/event_log.py +0 -316
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -407
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- 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
|
|
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
|
|
60
|
-
from lib.context.
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
111
|
+
You are about to lose context. Stop all other work and run `/handoff` immediately.
|
|
112
|
+
</system-reminder>"""
|
|
182
113
|
|
|
183
|
-
|
|
184
|
-
|
|
114
|
+
elif percent_remaining <= HANDOFF_PREPARE_THRESHOLD:
|
|
115
|
+
return f"""<system-reminder>
|
|
116
|
+
## LOW CONTEXT WARNING ({percent_remaining}% remaining)
|
|
185
117
|
|
|
186
|
-
|
|
118
|
+
{usage_line}
|
|
187
119
|
**Triggered by**: {tool_name} tool completion
|
|
188
120
|
|
|
189
|
-
|
|
190
|
-
{
|
|
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.
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
Check if context needs to transition to implementing mode.
|
|
134
|
+
{usage_line}
|
|
135
|
+
**Triggered by**: {tool_name} tool completion
|
|
203
136
|
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
if not
|
|
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(
|
|
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
|
|
191
|
+
return
|
|
281
192
|
except OSError:
|
|
282
193
|
pass
|
|
283
194
|
|
|
284
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
"""
|
|
329
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
39
|
+
from lib.context.context_store import (
|
|
40
40
|
get_context_by_session_id,
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
+
log_debug("file-suggestion", f"Found context by session: {context.id}")
|
|
151
151
|
return context.id
|
|
152
152
|
|
|
153
|
-
# Fall back to single
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
+
from lib.base.hook_utils import run_hook
|
|
215
|
+
run_hook(main, "file_suggestion")
|