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.
Files changed (116) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +3 -3
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  17. package/dist/templates/_shared/hooks/context_monitor.py +104 -247
  18. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  19. package/dist/templates/_shared/hooks/pre_compact.py +47 -32
  20. package/dist/templates/_shared/hooks/session_end.py +103 -60
  21. package/dist/templates/_shared/hooks/session_start.py +110 -81
  22. package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
  23. package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
  24. package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
  25. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  32. package/dist/templates/_shared/lib/base/inference.py +121 -0
  33. package/dist/templates/_shared/lib/base/logger.py +291 -0
  34. package/dist/templates/_shared/lib/base/utils.py +42 -9
  35. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  36. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  47. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  48. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  49. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  51. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  52. package/dist/templates/_shared/lib/templates/README.md +5 -13
  53. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  54. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  56. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  57. package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
  58. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  61. package/dist/templates/_shared/scripts/status_line.py +701 -0
  62. package/dist/templates/_shared/workflows/handoff.md +9 -3
  63. package/dist/templates/cc-native/.claude/settings.json +41 -8
  64. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  65. package/dist/templates/cc-native/MIGRATION.md +1 -1
  66. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
  68. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  71. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
  75. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
  76. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  79. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  87. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  96. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  97. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  98. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  99. package/oclif.manifest.json +1 -1
  100. package/package.json +1 -1
  101. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  102. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  103. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  104. package/dist/templates/_shared/lib/context/cache.py +0 -444
  105. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  106. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  107. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  108. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  109. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  110. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  111. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  115. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  116. 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