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.
Files changed (119) 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 +49 -18
  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_atomicity.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  18. package/dist/templates/_shared/hooks/context_monitor.py +128 -194
  19. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  20. package/dist/templates/_shared/hooks/pre_compact.py +104 -0
  21. package/dist/templates/_shared/hooks/session_end.py +154 -0
  22. package/dist/templates/_shared/hooks/session_start.py +145 -59
  23. package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
  24. package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
  25. package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
  26. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  32. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/base/constants.py +18 -4
  34. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  35. package/dist/templates/_shared/lib/base/inference.py +121 -0
  36. package/dist/templates/_shared/lib/base/logger.py +291 -0
  37. package/dist/templates/_shared/lib/base/utils.py +49 -11
  38. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  39. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  48. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  49. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  50. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  51. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  52. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  54. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  55. package/dist/templates/_shared/lib/templates/README.md +5 -13
  56. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  57. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
  61. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  62. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  63. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  64. package/dist/templates/_shared/scripts/status_line.py +701 -0
  65. package/dist/templates/_shared/workflows/handoff.md +9 -3
  66. package/dist/templates/cc-native/.claude/settings.json +64 -9
  67. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  68. package/dist/templates/cc-native/MIGRATION.md +1 -1
  69. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  70. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  71. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
  80. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  81. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  82. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  83. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  91. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  96. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  97. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  98. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  99. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  100. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  101. package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
  102. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  103. package/oclif.manifest.json +1 -1
  104. package/package.json +1 -1
  105. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  106. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
  107. package/dist/templates/_shared/lib/context/cache.py +0 -444
  108. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  109. package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
  110. package/dist/templates/_shared/lib/context/discovery.py +0 -444
  111. package/dist/templates/_shared/lib/context/event_log.py +0 -308
  112. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  113. package/dist/templates/_shared/lib/context/task_sync.py +0 -290
  114. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  115. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  116. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  117. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  118. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  119. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -1,205 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook for TaskCreate - assesses atomicity and forkability via inference.
3
-
4
- Ensures tasks contain sufficient self-contained context for independent execution,
5
- especially when delegated to subagents with zero conversation history.
6
-
7
- Non-blocking: Warns but allows creation even if atomicity is poor.
8
- """
9
-
10
- import json
11
- import sys
12
- from pathlib import Path
13
-
14
- # Path setup
15
- SCRIPT_DIR = Path(__file__).resolve().parent
16
- SHARED_LIB = SCRIPT_DIR.parent / "lib"
17
- sys.path.insert(0, str(SHARED_LIB.parent))
18
-
19
- from lib.base.hook_utils import (
20
- load_hook_input,
21
- validate_hook_event,
22
- get_tool_input,
23
- safe_hook_main,
24
- run_hook,
25
- )
26
- from lib.base.utils import eprint
27
- from lib.base.subprocess_utils import is_internal_call
28
- from lib.base.inference import inference
29
-
30
- # Prompt engineered per Prompting/Standards.md:
31
- # - Markdown-only (no XML)
32
- # - Positive framing (tell what TO do)
33
- # - 1-3 clear examples matching desired output
34
- # - Direct imperative instructions
35
- # - Explicit JSON output format
36
-
37
- ASSESSMENT_SYSTEM_PROMPT = """You assess task descriptions for atomicity and forkability.
38
-
39
- ## Definitions
40
-
41
- **Atomic Task:** Contains ALL context needed for independent execution without reading prior conversation.
42
-
43
- **Forkable Task:** Can be delegated to a subagent with ZERO conversation history and still be completed successfully.
44
-
45
- ## Signs of Non-Atomic Tasks
46
-
47
- Look for these indicators:
48
- - Contextual references: "the file above", "as discussed", "the mentioned function", "this bug"
49
- - Vague descriptions assuming prior knowledge: "fix the bug", "update it", "finish the work"
50
- - Missing specifics: which file? what function? what expected behavior? what error?
51
- - Pronouns without antecedents: "it", "they", "the issue" without explicit definition
52
-
53
- ## Signs of Atomic Tasks
54
-
55
- Well-specified tasks include:
56
- - Explicit file paths: "Edit src/utils/parser.py"
57
- - Specific function names: "Modify the validate_input() function"
58
- - Clear expected behavior: "Should return 404 when user not found"
59
- - Complete error context: "TypeError on line 45 when input is None"
60
-
61
- ## Examples
62
-
63
- **Example 1: Non-Atomic Task**
64
- Subject: "Fix the bug"
65
- Description: "The issue we discussed earlier needs to be resolved"
66
- Assessment: NOT atomic (no file, no function, no error details, references "discussed earlier")
67
-
68
- **Example 2: Atomic Task**
69
- Subject: "Fix null pointer in user lookup"
70
- Description: "In src/services/user.py, the get_user_by_id() function raises TypeError when user_id is None. Add null check at line 23 that returns None early instead of calling database.query()."
71
- Assessment: Atomic (file path, function name, specific error, exact fix location, expected behavior)
72
-
73
- **Example 3: Partially Atomic Task**
74
- Subject: "Add validation to form"
75
- Description: "Add email validation to the signup form. Return error message if invalid."
76
- Assessment: NOT fully atomic (missing: which file contains the form? what validation rules? where to display error?)
77
-
78
- ## Output Format
79
-
80
- Respond with valid JSON only:
81
- {
82
- "atomic": true/false,
83
- "forkable": true/false,
84
- "issues": ["specific issue 1", "specific issue 2"],
85
- "recommendation": "brief actionable suggestion if issues exist, or 'Task is well-specified' if good"
86
- }"""
87
-
88
- ASSESSMENT_USER_TEMPLATE = """Assess this task for atomicity and forkability:
89
-
90
- **Subject:** {subject}
91
-
92
- **Description:** {description}
93
-
94
- Evaluate whether a subagent with zero prior context could execute this task successfully."""
95
-
96
-
97
- @safe_hook_main("task_create_atomicity")
98
- def main() -> int:
99
- # Skip internal calls (prevents recursion from orchestrator/inference)
100
- if is_internal_call():
101
- return 0
102
-
103
- # Load and validate hook input
104
- payload = load_hook_input()
105
- if not payload:
106
- return 0
107
-
108
- # Only process TaskCreate
109
- if not validate_hook_event(payload, "PreToolUse", "TaskCreate"):
110
- return 0
111
-
112
- tool_input = get_tool_input(payload)
113
- if not tool_input:
114
- return 0
115
-
116
- subject = tool_input.get("subject", "")
117
- description = tool_input.get("description", "")
118
-
119
- # Skip very short tasks (likely intentionally brief or simple acknowledgments)
120
- if len(description.strip()) < 15:
121
- return 0
122
-
123
- # Call inference to assess atomicity and forkability
124
- result = inference(
125
- system_prompt=ASSESSMENT_SYSTEM_PROMPT,
126
- user_prompt=ASSESSMENT_USER_TEMPLATE.format(
127
- subject=subject,
128
- description=description
129
- ),
130
- level="fast", # Use Haiku for minimal latency (~1-2s)
131
- timeout=12, # Allow up to 12s for inference
132
- )
133
-
134
- if not result.success:
135
- eprint(f"[task_create_atomicity] Inference failed: {result.error}")
136
- return 0 # Non-blocking on failure
137
-
138
- # Parse JSON response
139
- try:
140
- # Handle potential markdown code blocks in response
141
- output = result.output.strip()
142
- if output.startswith("```"):
143
- # Extract JSON from code block
144
- lines = output.split("\n")
145
- json_lines = []
146
- in_block = False
147
- for line in lines:
148
- if line.startswith("```") and not in_block:
149
- in_block = True
150
- continue
151
- elif line.startswith("```") and in_block:
152
- break
153
- elif in_block:
154
- json_lines.append(line)
155
- output = "\n".join(json_lines)
156
-
157
- assessment = json.loads(output)
158
- except json.JSONDecodeError:
159
- eprint(f"[task_create_atomicity] Failed to parse inference response: {result.output[:100]}")
160
- return 0
161
-
162
- # Extract assessment fields
163
- atomic = assessment.get("atomic", True)
164
- forkable = assessment.get("forkable", True)
165
- issues = assessment.get("issues", [])
166
- recommendation = assessment.get("recommendation", "")
167
-
168
- # Build context message based on assessment
169
- if atomic and forkable:
170
- # Task is good - minimal positive feedback
171
- context_msg = "Task Assessment: Well-specified and forkable."
172
- else:
173
- # Task has issues - inject detailed warning
174
- status_parts = []
175
- if not atomic:
176
- status_parts.append("NOT ATOMIC")
177
- if not forkable:
178
- status_parts.append("NOT FORKABLE")
179
-
180
- issues_text = "\n".join(f"- {issue}" for issue in issues) if issues else "- See recommendation below"
181
-
182
- context_msg = f"""**TASK ATOMICITY WARNING** ({', '.join(status_parts)})
183
-
184
- This task may lack sufficient context for independent execution by a subagent.
185
-
186
- **Issues detected:**
187
- {issues_text}
188
-
189
- **Recommendation:** {recommendation}
190
-
191
- Consider adding specific file paths, function names, expected behaviors, or error details before creating this task."""
192
-
193
- # Output hook response with additionalContext
194
- out = {
195
- "hookSpecificOutput": {
196
- "hookEventName": "PreToolUse",
197
- "additionalContext": context_msg
198
- }
199
- }
200
- print(json.dumps(out, ensure_ascii=False))
201
- return 0
202
-
203
-
204
- if __name__ == "__main__":
205
- run_hook(main)
@@ -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