aiwcli 0.9.8 → 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 +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 +103 -60
- package/dist/templates/_shared/hooks/session_start.py +110 -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 +61 -61
- 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 +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 +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 +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 +1 -38
- 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 +41 -8
- 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 +49 -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 +57 -55
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- 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 +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
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
"""Cache rebuild utilities for context management.
|
|
2
|
-
|
|
3
|
-
These functions allow recovery from corrupted cache files
|
|
4
|
-
by rebuilding from the source of truth (events.jsonl).
|
|
5
|
-
|
|
6
|
-
Data hierarchy:
|
|
7
|
-
events.jsonl (source of truth)
|
|
8
|
-
→ context.json (L1 cache) - can be rebuilt
|
|
9
|
-
→ index.json (L2 cache) - can be rebuilt
|
|
10
|
-
"""
|
|
11
|
-
import json
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, Optional
|
|
14
|
-
|
|
15
|
-
from ..base.atomic_write import atomic_write
|
|
16
|
-
from ..base.constants import (
|
|
17
|
-
get_contexts_dir,
|
|
18
|
-
get_context_file_path,
|
|
19
|
-
get_events_file_path,
|
|
20
|
-
get_index_path,
|
|
21
|
-
get_archive_dir,
|
|
22
|
-
get_archive_index_path,
|
|
23
|
-
ARCHIVE_DIR,
|
|
24
|
-
)
|
|
25
|
-
from ..base.utils import eprint, now_iso
|
|
26
|
-
from .event_log import read_events
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def rebuild_context_from_events(context_dir: Path, project_root: Path = None) -> Optional['Context']:
|
|
30
|
-
"""
|
|
31
|
-
Rebuild context.json by replaying events.jsonl.
|
|
32
|
-
|
|
33
|
-
This is the recovery mechanism when context.json is
|
|
34
|
-
corrupted or out of sync.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
context_dir: Path to context directory
|
|
38
|
-
project_root: Project root directory (if known, avoids fragile path calculation)
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
Rebuilt Context object, or None if events file doesn't exist
|
|
42
|
-
"""
|
|
43
|
-
# Import here to avoid circular dependency
|
|
44
|
-
from .context_manager import Context, InFlightState
|
|
45
|
-
|
|
46
|
-
events_path = context_dir / "events.jsonl"
|
|
47
|
-
if not events_path.exists():
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
context_id = context_dir.name
|
|
51
|
-
|
|
52
|
-
# Calculate project_root if not provided
|
|
53
|
-
# Structure: project_root/_output/contexts/{id}
|
|
54
|
-
if project_root is None:
|
|
55
|
-
# Traverse up: context_dir -> contexts -> _output -> project_root
|
|
56
|
-
project_root = context_dir.parent.parent.parent
|
|
57
|
-
|
|
58
|
-
events = read_events(context_id, project_root)
|
|
59
|
-
|
|
60
|
-
if not events:
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
# Initialize context with defaults
|
|
64
|
-
context = Context(
|
|
65
|
-
id=context_id,
|
|
66
|
-
status="active",
|
|
67
|
-
folder=str(context_dir),
|
|
68
|
-
in_flight=InFlightState()
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
# Replay events to derive current state
|
|
72
|
-
for event in events:
|
|
73
|
-
event_type = event.get("event")
|
|
74
|
-
timestamp = event.get("timestamp")
|
|
75
|
-
|
|
76
|
-
# Update last_active for any event
|
|
77
|
-
context.last_active = timestamp
|
|
78
|
-
|
|
79
|
-
if event_type == "context_created":
|
|
80
|
-
context.summary = event.get("summary", "")
|
|
81
|
-
context.method = event.get("method")
|
|
82
|
-
context.tags = event.get("tags", [])
|
|
83
|
-
context.created_at = timestamp
|
|
84
|
-
|
|
85
|
-
elif event_type == "context_completed":
|
|
86
|
-
context.status = "completed"
|
|
87
|
-
|
|
88
|
-
elif event_type == "context_reopened":
|
|
89
|
-
context.status = "active"
|
|
90
|
-
|
|
91
|
-
elif event_type == "metadata_updated":
|
|
92
|
-
if "summary" in event:
|
|
93
|
-
context.summary = event["summary"]
|
|
94
|
-
if "tags" in event:
|
|
95
|
-
context.tags = event["tags"]
|
|
96
|
-
if "method" in event:
|
|
97
|
-
context.method = event["method"]
|
|
98
|
-
|
|
99
|
-
elif event_type == "planning_started":
|
|
100
|
-
context.in_flight.mode = "planning"
|
|
101
|
-
|
|
102
|
-
elif event_type == "plan_created":
|
|
103
|
-
context.in_flight.mode = "pending_implementation"
|
|
104
|
-
context.in_flight.artifact_path = event.get("path")
|
|
105
|
-
context.in_flight.artifact_hash = event.get("hash")
|
|
106
|
-
context.in_flight.started_at = timestamp
|
|
107
|
-
|
|
108
|
-
elif event_type == "plan_implementation_started":
|
|
109
|
-
context.in_flight.mode = "implementing"
|
|
110
|
-
|
|
111
|
-
elif event_type == "plan_completed":
|
|
112
|
-
context.in_flight.mode = "none"
|
|
113
|
-
context.in_flight.artifact_path = None
|
|
114
|
-
context.in_flight.artifact_hash = None
|
|
115
|
-
context.in_flight.started_at = None
|
|
116
|
-
|
|
117
|
-
elif event_type == "handoff_created":
|
|
118
|
-
# Handoff events are informational only - no mode change
|
|
119
|
-
context.in_flight.handoff_path = event.get("path")
|
|
120
|
-
|
|
121
|
-
elif event_type == "handoff_cleared":
|
|
122
|
-
# Legacy event - just clear handoff_path, no mode change
|
|
123
|
-
context.in_flight.handoff_path = None
|
|
124
|
-
|
|
125
|
-
return context
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def rebuild_index_from_folders(project_root: Path = None) -> Dict[str, Any]:
|
|
129
|
-
"""
|
|
130
|
-
Rebuild index.json by scanning context folders.
|
|
131
|
-
|
|
132
|
-
This is the recovery mechanism when index.json is
|
|
133
|
-
corrupted or out of sync.
|
|
134
|
-
|
|
135
|
-
Note: Skips the archive/ folder - archived contexts are not included
|
|
136
|
-
in the main index.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
project_root: Project root directory
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
Rebuilt index dictionary
|
|
143
|
-
"""
|
|
144
|
-
index = {
|
|
145
|
-
"version": "2.0",
|
|
146
|
-
"updated_at": now_iso(),
|
|
147
|
-
"contexts": {}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
contexts_dir = get_contexts_dir(project_root)
|
|
151
|
-
if not contexts_dir.exists():
|
|
152
|
-
return index
|
|
153
|
-
|
|
154
|
-
for ctx_dir in contexts_dir.iterdir():
|
|
155
|
-
if not ctx_dir.is_dir():
|
|
156
|
-
continue
|
|
157
|
-
|
|
158
|
-
# Skip archive folder - archived contexts have their own index
|
|
159
|
-
if ctx_dir.name == ARCHIVE_DIR:
|
|
160
|
-
continue
|
|
161
|
-
|
|
162
|
-
# Try to read context.json first
|
|
163
|
-
context_file = ctx_dir / "context.json"
|
|
164
|
-
if context_file.exists():
|
|
165
|
-
try:
|
|
166
|
-
ctx_data = json.loads(context_file.read_text(encoding='utf-8'))
|
|
167
|
-
in_flight = ctx_data.get("in_flight", {})
|
|
168
|
-
index["contexts"][ctx_data["id"]] = {
|
|
169
|
-
"id": ctx_data["id"],
|
|
170
|
-
"status": ctx_data.get("status", "active"),
|
|
171
|
-
"method": ctx_data.get("method"),
|
|
172
|
-
"summary": ctx_data.get("summary", ""),
|
|
173
|
-
"created_at": ctx_data.get("created_at"),
|
|
174
|
-
"last_active": ctx_data.get("last_active"),
|
|
175
|
-
"folder": str(ctx_dir),
|
|
176
|
-
"in_flight_mode": in_flight.get("mode", "none")
|
|
177
|
-
}
|
|
178
|
-
continue
|
|
179
|
-
except Exception as e:
|
|
180
|
-
eprint(f"[cache] Failed to read {context_file}, rebuilding from events: {e}")
|
|
181
|
-
|
|
182
|
-
# Fallback: rebuild from events
|
|
183
|
-
context = rebuild_context_from_events(ctx_dir)
|
|
184
|
-
if context:
|
|
185
|
-
index["contexts"][context.id] = context.to_index_entry()
|
|
186
|
-
|
|
187
|
-
return index
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def rebuild_archive_index(project_root: Path = None) -> Dict[str, Any]:
|
|
191
|
-
"""
|
|
192
|
-
Rebuild archive/index.json by scanning archive folder.
|
|
193
|
-
|
|
194
|
-
This is the recovery mechanism when archive index is
|
|
195
|
-
corrupted or out of sync.
|
|
196
|
-
|
|
197
|
-
Args:
|
|
198
|
-
project_root: Project root directory
|
|
199
|
-
|
|
200
|
-
Returns:
|
|
201
|
-
Rebuilt archive index dictionary
|
|
202
|
-
"""
|
|
203
|
-
archive_index = {
|
|
204
|
-
"version": "2.0",
|
|
205
|
-
"updated_at": now_iso(),
|
|
206
|
-
"contexts": {}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
archive_dir = get_archive_dir(project_root)
|
|
210
|
-
if not archive_dir.exists():
|
|
211
|
-
return archive_index
|
|
212
|
-
|
|
213
|
-
for ctx_dir in archive_dir.iterdir():
|
|
214
|
-
if not ctx_dir.is_dir():
|
|
215
|
-
continue
|
|
216
|
-
|
|
217
|
-
# Skip index.json file itself
|
|
218
|
-
if ctx_dir.name == "index.json":
|
|
219
|
-
continue
|
|
220
|
-
|
|
221
|
-
# Try to read context.json first
|
|
222
|
-
context_file = ctx_dir / "context.json"
|
|
223
|
-
if context_file.exists():
|
|
224
|
-
try:
|
|
225
|
-
ctx_data = json.loads(context_file.read_text(encoding='utf-8'))
|
|
226
|
-
in_flight = ctx_data.get("in_flight", {})
|
|
227
|
-
archive_index["contexts"][ctx_data["id"]] = {
|
|
228
|
-
"id": ctx_data["id"],
|
|
229
|
-
"status": ctx_data.get("status", "completed"),
|
|
230
|
-
"method": ctx_data.get("method"),
|
|
231
|
-
"summary": ctx_data.get("summary", ""),
|
|
232
|
-
"created_at": ctx_data.get("created_at"),
|
|
233
|
-
"last_active": ctx_data.get("last_active"),
|
|
234
|
-
"folder": str(ctx_dir),
|
|
235
|
-
"in_flight_mode": in_flight.get("mode", "none")
|
|
236
|
-
}
|
|
237
|
-
continue
|
|
238
|
-
except Exception as e:
|
|
239
|
-
eprint(f"[cache] Failed to read {context_file}, rebuilding from events: {e}")
|
|
240
|
-
|
|
241
|
-
# Fallback: rebuild from events
|
|
242
|
-
context = rebuild_context_from_events(ctx_dir)
|
|
243
|
-
if context:
|
|
244
|
-
archive_index["contexts"][context.id] = context.to_index_entry()
|
|
245
|
-
|
|
246
|
-
return archive_index
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def rebuild_all_caches(project_root: Path = None) -> bool:
|
|
250
|
-
"""
|
|
251
|
-
Rebuild all cache files from events.jsonl files.
|
|
252
|
-
|
|
253
|
-
Useful for recovery after corruption or version migration.
|
|
254
|
-
Rebuilds both active context caches and archive index.
|
|
255
|
-
|
|
256
|
-
Args:
|
|
257
|
-
project_root: Project root directory
|
|
258
|
-
|
|
259
|
-
Returns:
|
|
260
|
-
True if all caches were rebuilt successfully
|
|
261
|
-
"""
|
|
262
|
-
success = True
|
|
263
|
-
contexts_dir = get_contexts_dir(project_root)
|
|
264
|
-
|
|
265
|
-
if not contexts_dir.exists():
|
|
266
|
-
eprint("[cache] No contexts directory found, nothing to rebuild")
|
|
267
|
-
return True
|
|
268
|
-
|
|
269
|
-
# Rebuild each active context's cache (skip archive folder)
|
|
270
|
-
for ctx_dir in contexts_dir.iterdir():
|
|
271
|
-
if not ctx_dir.is_dir():
|
|
272
|
-
continue
|
|
273
|
-
|
|
274
|
-
# Skip archive folder - handled separately
|
|
275
|
-
if ctx_dir.name == ARCHIVE_DIR:
|
|
276
|
-
continue
|
|
277
|
-
|
|
278
|
-
events_path = ctx_dir / "events.jsonl"
|
|
279
|
-
if not events_path.exists():
|
|
280
|
-
continue
|
|
281
|
-
|
|
282
|
-
eprint(f"[cache] Rebuilding context: {ctx_dir.name}")
|
|
283
|
-
context = rebuild_context_from_events(ctx_dir)
|
|
284
|
-
|
|
285
|
-
if context:
|
|
286
|
-
context_file = ctx_dir / "context.json"
|
|
287
|
-
content = json.dumps(context.to_dict(), indent=2, ensure_ascii=False)
|
|
288
|
-
ok, error = atomic_write(context_file, content)
|
|
289
|
-
if not ok:
|
|
290
|
-
eprint(f"[cache] Failed to write {context_file}: {error}")
|
|
291
|
-
success = False
|
|
292
|
-
else:
|
|
293
|
-
eprint(f"[cache] Failed to rebuild context: {ctx_dir.name}")
|
|
294
|
-
success = False
|
|
295
|
-
|
|
296
|
-
# Rebuild archived context caches
|
|
297
|
-
archive_dir = get_archive_dir(project_root)
|
|
298
|
-
if archive_dir.exists():
|
|
299
|
-
for ctx_dir in archive_dir.iterdir():
|
|
300
|
-
if not ctx_dir.is_dir():
|
|
301
|
-
continue
|
|
302
|
-
|
|
303
|
-
events_path = ctx_dir / "events.jsonl"
|
|
304
|
-
if not events_path.exists():
|
|
305
|
-
continue
|
|
306
|
-
|
|
307
|
-
eprint(f"[cache] Rebuilding archived context: {ctx_dir.name}")
|
|
308
|
-
context = rebuild_context_from_events(ctx_dir)
|
|
309
|
-
|
|
310
|
-
if context:
|
|
311
|
-
context_file = ctx_dir / "context.json"
|
|
312
|
-
content = json.dumps(context.to_dict(), indent=2, ensure_ascii=False)
|
|
313
|
-
ok, error = atomic_write(context_file, content)
|
|
314
|
-
if not ok:
|
|
315
|
-
eprint(f"[cache] Failed to write {context_file}: {error}")
|
|
316
|
-
success = False
|
|
317
|
-
else:
|
|
318
|
-
eprint(f"[cache] Failed to rebuild archived context: {ctx_dir.name}")
|
|
319
|
-
success = False
|
|
320
|
-
|
|
321
|
-
# Rebuild global index
|
|
322
|
-
eprint("[cache] Rebuilding global index")
|
|
323
|
-
index = rebuild_index_from_folders(project_root)
|
|
324
|
-
index_path = get_index_path(project_root)
|
|
325
|
-
|
|
326
|
-
content = json.dumps(index, indent=2, ensure_ascii=False)
|
|
327
|
-
ok, error = atomic_write(index_path, content)
|
|
328
|
-
if not ok:
|
|
329
|
-
eprint(f"[cache] Failed to write index: {error}")
|
|
330
|
-
success = False
|
|
331
|
-
|
|
332
|
-
# Rebuild archive index
|
|
333
|
-
eprint("[cache] Rebuilding archive index")
|
|
334
|
-
archive_index = rebuild_archive_index(project_root)
|
|
335
|
-
archive_index_path = get_archive_index_path(project_root)
|
|
336
|
-
|
|
337
|
-
if archive_index["contexts"]: # Only write if there are archived contexts
|
|
338
|
-
content = json.dumps(archive_index, indent=2, ensure_ascii=False)
|
|
339
|
-
ok, error = atomic_write(archive_index_path, content)
|
|
340
|
-
if not ok:
|
|
341
|
-
eprint(f"[cache] Failed to write archive index: {error}")
|
|
342
|
-
success = False
|
|
343
|
-
|
|
344
|
-
total_contexts = len(index['contexts']) + len(archive_index['contexts'])
|
|
345
|
-
eprint(f"[cache] Rebuild complete. {len(index['contexts'])} active, {len(archive_index['contexts'])} archived contexts indexed.")
|
|
346
|
-
return success
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
def verify_cache_integrity(project_root: Path = None) -> Dict[str, Any]:
|
|
350
|
-
"""
|
|
351
|
-
Verify integrity of cache files against events.
|
|
352
|
-
|
|
353
|
-
Returns a report of any discrepancies found.
|
|
354
|
-
|
|
355
|
-
Args:
|
|
356
|
-
project_root: Project root directory
|
|
357
|
-
|
|
358
|
-
Returns:
|
|
359
|
-
Dictionary with verification results
|
|
360
|
-
"""
|
|
361
|
-
report = {
|
|
362
|
-
"ok": True,
|
|
363
|
-
"issues": [],
|
|
364
|
-
"contexts_checked": 0,
|
|
365
|
-
"contexts_with_issues": 0
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
contexts_dir = get_contexts_dir(project_root)
|
|
369
|
-
if not contexts_dir.exists():
|
|
370
|
-
return report
|
|
371
|
-
|
|
372
|
-
for ctx_dir in contexts_dir.iterdir():
|
|
373
|
-
if not ctx_dir.is_dir():
|
|
374
|
-
continue
|
|
375
|
-
|
|
376
|
-
# Skip archive folder - archived contexts verified separately
|
|
377
|
-
if ctx_dir.name == ARCHIVE_DIR:
|
|
378
|
-
continue
|
|
379
|
-
|
|
380
|
-
report["contexts_checked"] += 1
|
|
381
|
-
context_id = ctx_dir.name
|
|
382
|
-
|
|
383
|
-
# Check events.jsonl exists
|
|
384
|
-
events_path = ctx_dir / "events.jsonl"
|
|
385
|
-
if not events_path.exists():
|
|
386
|
-
report["issues"].append({
|
|
387
|
-
"context": context_id,
|
|
388
|
-
"issue": "Missing events.jsonl (source of truth)",
|
|
389
|
-
"severity": "critical"
|
|
390
|
-
})
|
|
391
|
-
report["contexts_with_issues"] += 1
|
|
392
|
-
report["ok"] = False
|
|
393
|
-
continue
|
|
394
|
-
|
|
395
|
-
# Check context.json exists
|
|
396
|
-
context_file = ctx_dir / "context.json"
|
|
397
|
-
if not context_file.exists():
|
|
398
|
-
report["issues"].append({
|
|
399
|
-
"context": context_id,
|
|
400
|
-
"issue": "Missing context.json (cache)",
|
|
401
|
-
"severity": "warning",
|
|
402
|
-
"action": "Run rebuild_all_caches()"
|
|
403
|
-
})
|
|
404
|
-
report["contexts_with_issues"] += 1
|
|
405
|
-
continue
|
|
406
|
-
|
|
407
|
-
# Compare cache with events-derived state
|
|
408
|
-
try:
|
|
409
|
-
cached = json.loads(context_file.read_text(encoding='utf-8'))
|
|
410
|
-
derived = rebuild_context_from_events(ctx_dir)
|
|
411
|
-
|
|
412
|
-
if derived:
|
|
413
|
-
has_issue = False
|
|
414
|
-
if cached.get("status") != derived.status:
|
|
415
|
-
report["issues"].append({
|
|
416
|
-
"context": context_id,
|
|
417
|
-
"issue": f"Status mismatch: cache={cached.get('status')}, events={derived.status}",
|
|
418
|
-
"severity": "warning"
|
|
419
|
-
})
|
|
420
|
-
has_issue = True
|
|
421
|
-
|
|
422
|
-
cached_mode = cached.get("in_flight", {}).get("mode", "none")
|
|
423
|
-
if cached_mode != derived.in_flight.mode:
|
|
424
|
-
report["issues"].append({
|
|
425
|
-
"context": context_id,
|
|
426
|
-
"issue": f"in_flight.mode mismatch: cache={cached_mode}, events={derived.in_flight.mode}",
|
|
427
|
-
"severity": "warning"
|
|
428
|
-
})
|
|
429
|
-
has_issue = True
|
|
430
|
-
|
|
431
|
-
if has_issue:
|
|
432
|
-
report["contexts_with_issues"] += 1
|
|
433
|
-
report["ok"] = False
|
|
434
|
-
|
|
435
|
-
except Exception as e:
|
|
436
|
-
report["issues"].append({
|
|
437
|
-
"context": context_id,
|
|
438
|
-
"issue": f"Verification error: {e}",
|
|
439
|
-
"severity": "error"
|
|
440
|
-
})
|
|
441
|
-
report["contexts_with_issues"] += 1
|
|
442
|
-
report["ok"] = False
|
|
443
|
-
|
|
444
|
-
return report
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
"""Context ID extraction from hook payloads.
|
|
2
|
-
|
|
3
|
-
Centralizes the logic for determining which context a hook operation
|
|
4
|
-
belongs to, using multiple fallback strategies.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any, Dict, Optional
|
|
9
|
-
|
|
10
|
-
from .context_manager import get_context_by_session_id, get_all_contexts
|
|
11
|
-
from ..base.utils import eprint
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def extract_context_id(
|
|
15
|
-
tool_input: Dict[str, Any],
|
|
16
|
-
project_root: Path,
|
|
17
|
-
session_id: Optional[str] = None,
|
|
18
|
-
hook_name: str = "hook",
|
|
19
|
-
check_persistent_id: bool = False
|
|
20
|
-
) -> Optional[str]:
|
|
21
|
-
"""
|
|
22
|
-
Extract context ID from tool input with multiple fallback strategies.
|
|
23
|
-
|
|
24
|
-
Order of precedence:
|
|
25
|
-
1. metadata.context field (explicit context specification)
|
|
26
|
-
2. Session ID lookup (session bound to context)
|
|
27
|
-
3. persistent_id parsing (if check_persistent_id=True, for TaskCreate)
|
|
28
|
-
4. Single active context fallback
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
tool_input: Tool input dict from hook payload
|
|
32
|
-
project_root: Project root directory
|
|
33
|
-
session_id: Optional session ID from hook payload
|
|
34
|
-
hook_name: Name of calling hook for log messages
|
|
35
|
-
check_persistent_id: Whether to check persistent_id for context hint
|
|
36
|
-
(used by task_create_capture for ID format parsing)
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
Context ID string, or None if context cannot be determined
|
|
40
|
-
"""
|
|
41
|
-
# 1. Check metadata.context field (explicit)
|
|
42
|
-
metadata = tool_input.get("metadata", {})
|
|
43
|
-
if isinstance(metadata, dict):
|
|
44
|
-
context = metadata.get("context")
|
|
45
|
-
if context:
|
|
46
|
-
return context
|
|
47
|
-
|
|
48
|
-
# 2. Check session ID (session may be bound to a context)
|
|
49
|
-
if session_id:
|
|
50
|
-
try:
|
|
51
|
-
session_context = get_context_by_session_id(session_id, project_root)
|
|
52
|
-
if session_context:
|
|
53
|
-
eprint(f"[{hook_name}] Found context via session_id: {session_context.id}")
|
|
54
|
-
return session_context.id
|
|
55
|
-
except Exception as e:
|
|
56
|
-
eprint(f"[{hook_name}] Failed to lookup context by session: {e}")
|
|
57
|
-
|
|
58
|
-
# 3. Check persistent_id for context hint (task_create only)
|
|
59
|
-
# Format: "context-id-task-N" or similar
|
|
60
|
-
if check_persistent_id and isinstance(metadata, dict):
|
|
61
|
-
persistent_id = metadata.get("persistent_id", "")
|
|
62
|
-
if persistent_id and "-" in persistent_id:
|
|
63
|
-
parts = persistent_id.split("-")
|
|
64
|
-
if len(parts) >= 2:
|
|
65
|
-
# Reconstruct context ID (everything before last two parts)
|
|
66
|
-
context_parts = parts[:-2] if len(parts) > 2 else parts[:1]
|
|
67
|
-
potential_id = "-".join(context_parts)
|
|
68
|
-
if potential_id:
|
|
69
|
-
return potential_id
|
|
70
|
-
|
|
71
|
-
# 4. Check for single active context (fallback)
|
|
72
|
-
try:
|
|
73
|
-
contexts = get_all_contexts(status="active", project_root=project_root)
|
|
74
|
-
if len(contexts) == 1:
|
|
75
|
-
return contexts[0].id
|
|
76
|
-
except Exception as e:
|
|
77
|
-
eprint(f"[{hook_name}] Failed to get active contexts: {e}")
|
|
78
|
-
|
|
79
|
-
return None
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def extract_context_id_for_session(
|
|
83
|
-
session_id: str,
|
|
84
|
-
project_root: Path,
|
|
85
|
-
hook_name: str = "hook"
|
|
86
|
-
) -> Optional[str]:
|
|
87
|
-
"""
|
|
88
|
-
Find context that matches this session_id.
|
|
89
|
-
|
|
90
|
-
Simpler variant for hooks that only need session-based lookup.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
session_id: Session ID to match
|
|
94
|
-
project_root: Project root directory
|
|
95
|
-
hook_name: Name of calling hook for log messages
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
Context ID or None if not found
|
|
99
|
-
"""
|
|
100
|
-
contexts = get_all_contexts(status="active", project_root=project_root)
|
|
101
|
-
|
|
102
|
-
# Primary strategy: Find context with matching session_id
|
|
103
|
-
for ctx in contexts:
|
|
104
|
-
if ctx.in_flight and ctx.in_flight.session_ids and session_id in ctx.in_flight.session_ids:
|
|
105
|
-
eprint(f"[{hook_name}] Found context by session: {ctx.id}")
|
|
106
|
-
return ctx.id
|
|
107
|
-
|
|
108
|
-
# Fallback: If only one context is planning, assume it's the one
|
|
109
|
-
planning_contexts = [c for c in contexts if c.in_flight and c.in_flight.mode == "planning"]
|
|
110
|
-
if len(planning_contexts) == 1:
|
|
111
|
-
eprint(f"[{hook_name}] Fallback: Single planning context: {planning_contexts[0].id}")
|
|
112
|
-
return planning_contexts[0].id
|
|
113
|
-
|
|
114
|
-
eprint(f"[{hook_name}] Could not find context for session {session_id}")
|
|
115
|
-
return None
|