aiwcli 0.9.7 → 0.10.0
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 +49 -18
- 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_atomicity.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 +128 -194
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +104 -0
- package/dist/templates/_shared/hooks/session_end.py +154 -0
- package/dist/templates/_shared/hooks/session_start.py +145 -59
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
- package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
- 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__/constants.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/constants.py +18 -4
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -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 +49 -11
- 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 +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -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 +25 -79
- 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 +39 -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 +64 -9
- 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/agents/CLAUDE.md +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
- 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 +57 -57
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- 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 +35 -10
- 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 +103 -42
- 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 +210 -43
- 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 -205
- 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 -1054
- package/dist/templates/_shared/lib/context/discovery.py +0 -444
- package/dist/templates/_shared/lib/context/event_log.py +0 -308
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -290
- 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,292 +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
65
|
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
CRITICAL_CONTEXT_THRESHOLD = 25 # Urgent warning below 25%
|
|
69
|
-
|
|
70
|
-
# Context baseline: preloaded tokens not visible to hooks (~22.6k typical)
|
|
71
|
-
# This includes system prompt, tools, MCP tokens that aren't in hook data
|
|
72
|
-
CONTEXT_BASELINE = 22_600
|
|
73
|
-
|
|
74
|
-
# Default context window size (used when not provided in hook input)
|
|
75
|
-
DEFAULT_CONTEXT_WINDOW = 200_000
|
|
66
|
+
# Module-level flag: only save auto-state once per process lifetime
|
|
67
|
+
_PROGRESSIVE_SAVE_MARKER = ".progressive-save-done"
|
|
76
68
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Claude Code provides context_window data with actual token counts:
|
|
83
|
-
- cache_read_input_tokens: Tokens read from cache
|
|
84
|
-
- input_tokens: New input tokens
|
|
85
|
-
- cache_creation_input_tokens: Tokens written to cache
|
|
86
|
-
- output_tokens: Model output tokens
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
hook_input: Hook input data from Claude Code
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
Tuple of (tokens_used, max_tokens) or (None, None) if not available
|
|
93
|
-
"""
|
|
94
|
-
context_window = hook_input.get("context_window")
|
|
95
|
-
if not context_window:
|
|
96
|
-
return None, None
|
|
97
|
-
|
|
98
|
-
current_usage = context_window.get("current_usage")
|
|
99
|
-
if not current_usage:
|
|
100
|
-
return None, None
|
|
101
|
-
|
|
102
|
-
# Sum all token types
|
|
103
|
-
cache_read = current_usage.get("cache_read_input_tokens", 0) or 0
|
|
104
|
-
input_tokens = current_usage.get("input_tokens", 0) or 0
|
|
105
|
-
cache_creation = current_usage.get("cache_creation_input_tokens", 0) or 0
|
|
106
|
-
output_tokens = current_usage.get("output_tokens", 0) or 0
|
|
107
|
-
|
|
108
|
-
content_tokens = cache_read + input_tokens + cache_creation + output_tokens
|
|
109
|
-
|
|
110
|
-
# Add baseline for system prompt, tools, MCP tokens not in hook data
|
|
111
|
-
tokens_used = content_tokens + CONTEXT_BASELINE
|
|
112
|
-
|
|
113
|
-
# Get max context window from hook input
|
|
114
|
-
max_tokens = context_window.get("context_window_size") or DEFAULT_CONTEXT_WINDOW
|
|
115
|
-
|
|
116
|
-
return tokens_used, max_tokens
|
|
69
|
+
# Configuration
|
|
70
|
+
SAVE_STATE_THRESHOLD = 60 # Silently save auto-state at 60% remaining
|
|
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)
|
|
117
74
|
|
|
118
75
|
|
|
119
76
|
def get_current_context_id(project_root: Path = None) -> Optional[str]:
|
|
120
|
-
"""
|
|
121
|
-
Determine the current active context.
|
|
122
|
-
|
|
123
|
-
Falls back to most recently active context.
|
|
124
|
-
|
|
125
|
-
Returns:
|
|
126
|
-
Context ID or None if no active context
|
|
127
|
-
"""
|
|
77
|
+
"""Determine the current active context (most recently active)."""
|
|
128
78
|
contexts = get_all_contexts(status="active", project_root=project_root)
|
|
129
79
|
if contexts:
|
|
130
|
-
return contexts[0].id
|
|
80
|
+
return contexts[0].id
|
|
131
81
|
return None
|
|
132
82
|
|
|
133
83
|
|
|
134
84
|
def get_context_warning(
|
|
135
85
|
percent_remaining: int,
|
|
136
|
-
tokens_used: int,
|
|
137
|
-
max_tokens: int,
|
|
86
|
+
tokens_used: Optional[int],
|
|
87
|
+
max_tokens: Optional[int],
|
|
138
88
|
context_id: Optional[str],
|
|
139
89
|
tool_name: str
|
|
140
90
|
) -> str:
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
context_id: Current context ID (if any)
|
|
149
|
-
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"
|
|
150
98
|
|
|
151
|
-
|
|
152
|
-
System reminder markdown
|
|
153
|
-
"""
|
|
154
|
-
# Format token counts
|
|
155
|
-
tokens_used_k = tokens_used // 1000
|
|
156
|
-
max_tokens_k = max_tokens // 1000
|
|
99
|
+
context_line = f"\nContext ID: `{context_id}`" if context_id else ""
|
|
157
100
|
|
|
158
101
|
if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
else:
|
|
162
|
-
urgency = "LOW"
|
|
163
|
-
instruction = "Please wrap up your current task and prepare for handoff."
|
|
102
|
+
return f"""<system-reminder>
|
|
103
|
+
## CRITICAL CONTEXT WARNING ({percent_remaining}% remaining)
|
|
164
104
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- What's completed
|
|
171
|
-
- What still needs to be done
|
|
172
|
-
- Any important decisions or context
|
|
105
|
+
{usage_line}
|
|
106
|
+
**Triggered by**: {tool_name} tool completion
|
|
107
|
+
|
|
108
|
+
**CRITICAL: Run `/handoff` now before context is compacted.**
|
|
109
|
+
{context_line}
|
|
173
110
|
|
|
174
|
-
|
|
111
|
+
You are about to lose context. Stop all other work and run `/handoff` immediately.
|
|
112
|
+
</system-reminder>"""
|
|
175
113
|
|
|
176
|
-
|
|
177
|
-
|
|
114
|
+
elif percent_remaining <= HANDOFF_PREPARE_THRESHOLD:
|
|
115
|
+
return f"""<system-reminder>
|
|
116
|
+
## LOW CONTEXT WARNING ({percent_remaining}% remaining)
|
|
178
117
|
|
|
179
|
-
|
|
118
|
+
{usage_line}
|
|
180
119
|
**Triggered by**: {tool_name} tool completion
|
|
181
120
|
|
|
182
|
-
|
|
183
|
-
{
|
|
121
|
+
**Context is getting low. Please finish your current task and run `/handoff`.**
|
|
122
|
+
{context_line}
|
|
184
123
|
|
|
185
124
|
**Actions:**
|
|
186
125
|
1. Complete your current atomic task (if 1-2 steps away)
|
|
187
126
|
2. Do NOT start new multi-step work
|
|
188
|
-
3.
|
|
189
|
-
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
|
|
190
128
|
</system-reminder>"""
|
|
191
129
|
|
|
130
|
+
else:
|
|
131
|
+
return f"""<system-reminder>
|
|
132
|
+
## CONTEXT NOTICE ({percent_remaining}% remaining)
|
|
133
|
+
|
|
134
|
+
{usage_line}
|
|
135
|
+
**Triggered by**: {tool_name} tool completion
|
|
192
136
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
Check if context needs to transition to implementing mode.
|
|
137
|
+
**Consider preparing a handoff soon. When ready, run `/handoff` to generate a handoff document.**
|
|
138
|
+
{context_line}
|
|
196
139
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
2. Plan was in planning mode but permission_mode is no longer "plan"
|
|
200
|
-
(e.g., after /clear which clears permissions and pastes the plan)
|
|
140
|
+
Continue your current work, but avoid starting large new tasks.
|
|
141
|
+
</system-reminder>"""
|
|
201
142
|
|
|
202
|
-
If we're seeing tool usage (Edit, Write, Bash) and either:
|
|
203
|
-
- Context is in "pending_implementation", OR
|
|
204
|
-
- Context is in "planning" and permission_mode is not "plan"
|
|
205
|
-
We transition to "implementing".
|
|
206
143
|
|
|
207
|
-
|
|
208
|
-
hook_input: Hook input data from Claude Code
|
|
144
|
+
def check_and_transition_mode(hook_input: dict) -> None:
|
|
209
145
|
"""
|
|
210
|
-
|
|
211
|
-
implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
|
|
212
|
-
tool_name = hook_input.get("tool_name", "")
|
|
213
|
-
|
|
214
|
-
if tool_name not in implementation_tools:
|
|
215
|
-
return
|
|
146
|
+
Check if context mode needs to transition based on tool usage.
|
|
216
147
|
|
|
148
|
+
Handles:
|
|
149
|
+
- has_plan + implementation tool -> active (started implementing)
|
|
150
|
+
- idle + implementation tool -> active
|
|
151
|
+
"""
|
|
217
152
|
project_root = project_dir(hook_input)
|
|
218
153
|
session_id = hook_input.get("session_id")
|
|
219
154
|
|
|
220
155
|
if not session_id:
|
|
221
156
|
return
|
|
222
157
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if not context:
|
|
158
|
+
state = get_context_by_session_id(session_id, project_root)
|
|
159
|
+
if not state:
|
|
226
160
|
return
|
|
227
161
|
|
|
228
|
-
|
|
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:
|
|
229
167
|
return
|
|
230
168
|
|
|
231
|
-
current_mode = context.in_flight.mode
|
|
232
169
|
permission_mode = hook_input.get("permission_mode", "default")
|
|
170
|
+
maybe_activate(state.id, permission_mode, project_root=project_root, caller="context_monitor")
|
|
233
171
|
|
|
234
|
-
# Transition from pending_implementation to implementing
|
|
235
|
-
if current_mode == "pending_implementation":
|
|
236
|
-
eprint(f"[context_monitor] Transitioning {context.id} from pending_implementation to implementing")
|
|
237
|
-
update_plan_status(context.id, "implementing", project_root=project_root)
|
|
238
172
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
173
|
+
def _try_progressive_save(hook_input: dict, percent_remaining: int) -> None:
|
|
174
|
+
"""Silently save state at SAVE_STATE_THRESHOLD (60%)."""
|
|
175
|
+
try:
|
|
176
|
+
session_id = hook_input.get("session_id", "")
|
|
177
|
+
if not session_id:
|
|
178
|
+
return
|
|
243
179
|
|
|
180
|
+
project_root = project_dir(hook_input)
|
|
181
|
+
state = get_context_by_session_id(session_id, project_root)
|
|
182
|
+
if not state:
|
|
183
|
+
return
|
|
244
184
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
185
|
+
from lib.base.constants import get_context_dir
|
|
186
|
+
marker_path = get_context_dir(state.id, project_root) / _PROGRESSIVE_SAVE_MARKER
|
|
187
|
+
if marker_path.exists():
|
|
188
|
+
try:
|
|
189
|
+
saved_session = marker_path.read_text(encoding="utf-8").strip()
|
|
190
|
+
if saved_session == session_id:
|
|
191
|
+
return
|
|
192
|
+
except OSError:
|
|
193
|
+
pass
|
|
248
194
|
|
|
249
|
-
|
|
195
|
+
log_info("context_monitor", f"Progressive save at {percent_remaining}% remaining")
|
|
250
196
|
|
|
251
|
-
|
|
252
|
-
|
|
197
|
+
# Just update last_active and save state
|
|
198
|
+
state.last_active = now_iso()
|
|
199
|
+
save_state(state, project_root)
|
|
253
200
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
201
|
+
try:
|
|
202
|
+
marker_path.write_text(session_id, encoding="utf-8")
|
|
203
|
+
except OSError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
log_warn("context_monitor", f"Progressive save error (non-fatal): {e}")
|
|
258
208
|
|
|
259
|
-
# 1. Try to get context_window data (fast dict access)
|
|
260
|
-
tokens_used, max_tokens = get_context_tokens_from_hook(hook_input)
|
|
261
209
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return None
|
|
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)
|
|
267
214
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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})
|
|
218
|
+
|
|
219
|
+
if percent_remaining is None:
|
|
220
|
+
return None
|
|
271
221
|
|
|
272
|
-
|
|
273
|
-
if percent_remaining > LOW_CONTEXT_THRESHOLD:
|
|
222
|
+
if percent_remaining > SAVE_STATE_THRESHOLD:
|
|
274
223
|
return None
|
|
275
224
|
|
|
276
|
-
|
|
225
|
+
if percent_remaining > HANDOFF_SUGGEST_THRESHOLD:
|
|
226
|
+
_try_progressive_save(hook_input, percent_remaining)
|
|
227
|
+
return None
|
|
277
228
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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)")
|
|
281
234
|
|
|
282
|
-
# Get current context for handoff info (file I/O)
|
|
283
235
|
project_root = project_dir(hook_input)
|
|
284
236
|
context_id = get_current_context_id(project_root)
|
|
285
237
|
|
|
286
|
-
|
|
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})
|
|
287
244
|
|
|
288
|
-
return get_context_warning(
|
|
289
|
-
percent_remaining,
|
|
290
|
-
tokens_used,
|
|
291
|
-
max_tokens,
|
|
292
|
-
context_id,
|
|
293
|
-
tool_name
|
|
294
|
-
)
|
|
245
|
+
return get_context_warning(percent_remaining, tokens_used, max_tokens, context_id, tool_name)
|
|
295
246
|
|
|
296
247
|
|
|
297
248
|
def main():
|
|
298
|
-
"""
|
|
299
|
-
Main entry point for PostToolUse hook.
|
|
300
|
-
|
|
301
|
-
Reads hook input from stdin, checks for mode transitions,
|
|
302
|
-
and prints system reminder if context is low.
|
|
303
|
-
"""
|
|
249
|
+
"""Main entry point for PostToolUse hook."""
|
|
304
250
|
try:
|
|
305
|
-
# Read hook input using shared utility
|
|
306
251
|
hook_input = load_hook_input()
|
|
307
|
-
|
|
308
252
|
if not hook_input:
|
|
309
253
|
return
|
|
310
254
|
|
|
311
|
-
# Always check for mode transitions on implementation tools
|
|
312
|
-
# This handles the case where /clear pastes the plan with non-plan permission mode
|
|
313
255
|
check_and_transition_mode(hook_input)
|
|
314
256
|
|
|
315
|
-
# Check context level
|
|
316
257
|
warning = check_context_level(hook_input)
|
|
317
|
-
|
|
318
258
|
if warning:
|
|
319
|
-
|
|
320
|
-
# Plain stdout from PostToolUse only goes to verbose mode, not Claude's context
|
|
321
|
-
output = {
|
|
322
|
-
"hookSpecificOutput": {
|
|
323
|
-
"hookEventName": "PostToolUse",
|
|
324
|
-
"additionalContext": warning
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
print(json.dumps(output))
|
|
259
|
+
emit_context(warning)
|
|
328
260
|
|
|
329
261
|
except Exception as e:
|
|
330
|
-
eprint(f"[context_monitor] ERROR: {e}")
|
|
331
262
|
import traceback
|
|
332
|
-
|
|
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)
|
|
333
266
|
|
|
334
267
|
|
|
335
268
|
if __name__ == "__main__":
|
|
336
|
-
|
|
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")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreCompact hook - saves auto-state before context compaction.
|
|
3
|
+
|
|
4
|
+
Critical: saves state before context compaction destroys token history.
|
|
5
|
+
After compaction, SessionStart fires with source="compact" and the
|
|
6
|
+
restored auto-state provides continuity context.
|
|
7
|
+
|
|
8
|
+
Hook input (from Claude Code):
|
|
9
|
+
{
|
|
10
|
+
"hook_event_name": "PreCompact",
|
|
11
|
+
"session_id": "abc123",
|
|
12
|
+
"transcript_path": "/path/to/transcript.jsonl",
|
|
13
|
+
"cwd": "/path/to/project",
|
|
14
|
+
...
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Hook output:
|
|
18
|
+
- Silent (no stdout output needed)
|
|
19
|
+
- Logs to stderr for debugging
|
|
20
|
+
"""
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Add parent directories to path for imports
|
|
25
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
26
|
+
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
27
|
+
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
28
|
+
|
|
29
|
+
from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_error
|
|
30
|
+
from lib.base.utils import project_dir
|
|
31
|
+
from lib.context.context_store import get_context_by_session_id, save_state
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
"""Save auto-state before compaction."""
|
|
36
|
+
try:
|
|
37
|
+
hook_input = load_hook_input()
|
|
38
|
+
if not hook_input:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
session_id = hook_input.get("session_id", "")
|
|
42
|
+
transcript_path = hook_input.get("transcript_path")
|
|
43
|
+
project_root = project_dir(hook_input)
|
|
44
|
+
|
|
45
|
+
if not session_id:
|
|
46
|
+
log_debug("pre_compact", "No session_id, skipping")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
log_info("pre_compact", f"Saving state before compaction: {session_id[:8]}...")
|
|
50
|
+
|
|
51
|
+
# Find context bound to this session
|
|
52
|
+
context = get_context_by_session_id(session_id, project_root)
|
|
53
|
+
if not context:
|
|
54
|
+
log_debug("pre_compact", "No context bound to this session, skipping")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Save last_session snapshot directly to state.json
|
|
58
|
+
import subprocess
|
|
59
|
+
git_state = {}
|
|
60
|
+
try:
|
|
61
|
+
branch = subprocess.run(
|
|
62
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
63
|
+
capture_output=True, text=True, timeout=5
|
|
64
|
+
)
|
|
65
|
+
git_state["branch"] = branch.stdout.strip() if branch.returncode == 0 else "unknown"
|
|
66
|
+
|
|
67
|
+
status = subprocess.run(
|
|
68
|
+
["git", "status", "--short"],
|
|
69
|
+
capture_output=True, text=True, timeout=5
|
|
70
|
+
)
|
|
71
|
+
if status.returncode == 0 and status.stdout.strip():
|
|
72
|
+
git_state["uncommitted_files"] = [
|
|
73
|
+
line.split(None, 1)[-1] for line in status.stdout.strip().split("\n")[:10]
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
log = subprocess.run(
|
|
77
|
+
["git", "log", "-1", "--format=%h %s"],
|
|
78
|
+
capture_output=True, text=True, timeout=5
|
|
79
|
+
)
|
|
80
|
+
if log.returncode == 0:
|
|
81
|
+
git_state["last_commit_short"] = log.stdout.strip()
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
from lib.base.utils import now_iso
|
|
86
|
+
context.last_session = {
|
|
87
|
+
"session_id": session_id,
|
|
88
|
+
"saved_at": now_iso(),
|
|
89
|
+
"save_reason": "pre_compact",
|
|
90
|
+
"git_state": git_state,
|
|
91
|
+
}
|
|
92
|
+
save_state(context, project_root)
|
|
93
|
+
log_info("pre_compact", f"State saved for {context.id}")
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
import traceback
|
|
97
|
+
tb = traceback.format_exc()
|
|
98
|
+
from lib.base.hook_utils import log_hook_error
|
|
99
|
+
log_hook_error("pre_compact", e, "PreCompact", traceback_str=tb)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
from lib.base.hook_utils import run_hook
|
|
104
|
+
run_hook(main, "pre_compact")
|