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,308 +0,0 @@
1
- """Event log utilities for context management.
2
-
3
- events.jsonl is the SOURCE OF TRUTH for each context.
4
- All state is derived by replaying these events.
5
-
6
- Event format (one JSON object per line):
7
- {"event": "event_type", "timestamp": "ISO8601", ...event-specific fields}
8
-
9
- Crash safety:
10
- - Append-only file
11
- - Each line is independent JSON
12
- - Corrupted lines are skipped with warning
13
- - Valid events before corruption are preserved
14
- """
15
- import json
16
- from dataclasses import dataclass, field
17
- from datetime import datetime
18
- from pathlib import Path
19
- from typing import Any, Dict, List, Optional
20
-
21
- from ..base.atomic_write import atomic_append
22
- from ..base.constants import get_events_file_path
23
- from ..base.utils import eprint, now_iso
24
-
25
-
26
- # Event type constants
27
- EVENT_CONTEXT_CREATED = "context_created"
28
- EVENT_CONTEXT_COMPLETED = "context_completed"
29
- EVENT_CONTEXT_REOPENED = "context_reopened"
30
- EVENT_CONTEXT_ARCHIVED = "context_archived"
31
- EVENT_METADATA_UPDATED = "metadata_updated"
32
- EVENT_TASK_ADDED = "task_added"
33
- EVENT_TASK_STARTED = "task_started"
34
- EVENT_TASK_COMPLETED = "task_completed"
35
- EVENT_TASK_BLOCKED = "task_blocked"
36
- EVENT_NOTE_ADDED = "note_added"
37
- EVENT_SESSION_STARTED = "session_started"
38
- EVENT_PLANNING_STARTED = "planning_started"
39
- EVENT_PLAN_CREATED = "plan_created"
40
- EVENT_PLAN_IMPLEMENTATION_STARTED = "plan_implementation_started"
41
- EVENT_PLAN_COMPLETED = "plan_completed"
42
- EVENT_HANDOFF_CREATED = "handoff_created"
43
- EVENT_HANDOFF_CLEARED = "handoff_cleared"
44
-
45
-
46
- @dataclass
47
- class Task:
48
- """Task state derived from events."""
49
- id: str
50
- subject: str
51
- description: str = ""
52
- active_form: str = ""
53
- status: str = "pending" # pending, in_progress, completed, blocked
54
- evidence: str = ""
55
- work_summary: str = ""
56
- files_changed: List[str] = field(default_factory=list)
57
- blocked_reason: str = ""
58
-
59
-
60
- @dataclass
61
- class ContextState:
62
- """Current state of a context, derived from events."""
63
- id: str
64
- status: str = "active" # active, completed
65
- summary: str = ""
66
- method: Optional[str] = None
67
- tags: List[str] = field(default_factory=list)
68
- created_at: Optional[str] = None
69
- last_active: Optional[str] = None
70
- tasks: List[Task] = field(default_factory=list)
71
- notes: List[str] = field(default_factory=list)
72
- plan_status: str = "none" # none, planning, pending_implementation, implementing
73
- plan_path: Optional[str] = None
74
- plan_hash: Optional[str] = None
75
-
76
-
77
- def read_events(context_id: str, project_root: Path = None) -> List[Dict[str, Any]]:
78
- """
79
- Read all events from a context's events.jsonl file.
80
-
81
- Handles corrupted lines gracefully by skipping them with a warning.
82
-
83
- Args:
84
- context_id: Context identifier
85
- project_root: Project root directory (default: cwd)
86
-
87
- Returns:
88
- List of event dictionaries, in chronological order
89
- """
90
- events_path = get_events_file_path(context_id, project_root)
91
-
92
- if not events_path.exists():
93
- return []
94
-
95
- events = []
96
- try:
97
- content = events_path.read_text(encoding='utf-8')
98
- for line_num, line in enumerate(content.splitlines(), 1):
99
- line = line.strip()
100
- if not line:
101
- continue
102
-
103
- try:
104
- event = json.loads(line)
105
- events.append(event)
106
- except json.JSONDecodeError:
107
- eprint(f"[event_log] WARNING: Skipping corrupted line {line_num} in {events_path}")
108
-
109
- except UnicodeDecodeError as e:
110
- eprint(f"[event_log] WARNING: Invalid UTF-8 in events file {events_path}, attempting fallback read")
111
- # Try reading with error handling to salvage what we can
112
- try:
113
- content = events_path.read_text(encoding='utf-8', errors='replace')
114
- for line_num, line in enumerate(content.splitlines(), 1):
115
- line = line.strip()
116
- if not line:
117
- continue
118
-
119
- try:
120
- event = json.loads(line)
121
- events.append(event)
122
- except json.JSONDecodeError:
123
- eprint(f"[event_log] WARNING: Skipping corrupted line {line_num} in {events_path}")
124
- except Exception as fallback_error:
125
- eprint(f"[event_log] ERROR: Fallback read failed: {fallback_error}")
126
-
127
- except Exception as e:
128
- eprint(f"[event_log] ERROR reading events file: {e}")
129
-
130
- return events
131
-
132
-
133
- def append_event(
134
- context_id: str,
135
- event_type: str,
136
- project_root: Path = None,
137
- **event_data
138
- ) -> bool:
139
- """
140
- Append an event to a context's events.jsonl file.
141
-
142
- Args:
143
- context_id: Context identifier
144
- event_type: Type of event (e.g., "task_added", "context_completed")
145
- project_root: Project root directory (default: cwd)
146
- **event_data: Additional event-specific data
147
-
148
- Returns:
149
- True if event was successfully appended
150
- """
151
- events_path = get_events_file_path(context_id, project_root)
152
-
153
- event = {
154
- "event": event_type,
155
- "timestamp": now_iso(),
156
- **event_data
157
- }
158
-
159
- try:
160
- event_json = json.dumps(event, ensure_ascii=False)
161
- success, error = atomic_append(events_path, event_json + "\n")
162
-
163
- if not success:
164
- eprint(f"[event_log] ERROR appending event: {error}")
165
- return False
166
-
167
- return True
168
-
169
- except Exception as e:
170
- eprint(f"[event_log] ERROR serializing event: {e}")
171
- return False
172
-
173
-
174
- def get_current_state(context_id: str, project_root: Path = None) -> ContextState:
175
- """
176
- Compute current context state by replaying events.
177
-
178
- This is the canonical way to determine current state -
179
- everything is derived from events.jsonl.
180
-
181
- Args:
182
- context_id: Context identifier
183
- project_root: Project root directory (default: cwd)
184
-
185
- Returns:
186
- ContextState representing current state
187
- """
188
- events = read_events(context_id, project_root)
189
-
190
- state = ContextState(id=context_id)
191
- tasks_map: Dict[str, Task] = {}
192
-
193
- for event in events:
194
- event_type = event.get("event")
195
- timestamp = event.get("timestamp")
196
-
197
- # Update last_active for any event
198
- state.last_active = timestamp
199
-
200
- if event_type == EVENT_CONTEXT_CREATED:
201
- state.summary = event.get("summary", "")
202
- state.method = event.get("method")
203
- state.tags = event.get("tags", [])
204
- state.created_at = timestamp
205
-
206
- elif event_type == EVENT_CONTEXT_COMPLETED:
207
- state.status = "completed"
208
-
209
- elif event_type == EVENT_CONTEXT_REOPENED:
210
- state.status = "active"
211
-
212
- elif event_type == EVENT_METADATA_UPDATED:
213
- if "summary" in event:
214
- state.summary = event["summary"]
215
- if "tags" in event:
216
- state.tags = event["tags"]
217
- if "method" in event:
218
- state.method = event["method"]
219
-
220
- elif event_type == EVENT_TASK_ADDED:
221
- task_id = event.get("task_id")
222
- if task_id:
223
- tasks_map[task_id] = Task(
224
- id=task_id,
225
- subject=event.get("subject", ""),
226
- description=event.get("description", ""),
227
- active_form=event.get("activeForm", ""),
228
- status="pending"
229
- )
230
-
231
- elif event_type == EVENT_TASK_STARTED:
232
- task_id = event.get("task_id")
233
- if task_id and task_id in tasks_map:
234
- tasks_map[task_id].status = "in_progress"
235
-
236
- elif event_type == EVENT_TASK_COMPLETED:
237
- task_id = event.get("task_id")
238
- if task_id and task_id in tasks_map:
239
- task = tasks_map[task_id]
240
- task.status = "completed"
241
- task.evidence = event.get("evidence", "")
242
- task.work_summary = event.get("work_summary", "")
243
- task.files_changed = event.get("files_changed", [])
244
-
245
- elif event_type == EVENT_TASK_BLOCKED:
246
- task_id = event.get("task_id")
247
- if task_id and task_id in tasks_map:
248
- tasks_map[task_id].status = "blocked"
249
- tasks_map[task_id].blocked_reason = event.get("reason", "")
250
-
251
- elif event_type == EVENT_NOTE_ADDED:
252
- note = event.get("content", "")
253
- if note:
254
- state.notes.append(note)
255
-
256
- elif event_type == EVENT_PLAN_CREATED:
257
- state.plan_status = "pending_implementation"
258
- state.plan_path = event.get("path")
259
- state.plan_hash = event.get("hash")
260
-
261
- elif event_type == EVENT_PLAN_IMPLEMENTATION_STARTED:
262
- state.plan_status = "implementing"
263
-
264
- elif event_type == EVENT_PLAN_COMPLETED:
265
- state.plan_status = "none"
266
- state.plan_path = None
267
- state.plan_hash = None
268
-
269
- # Convert tasks map to list
270
- state.tasks = list(tasks_map.values())
271
-
272
- return state
273
-
274
-
275
- def are_all_tasks_completed(context_id: str, project_root: Path = None) -> bool:
276
- """
277
- Check if all tasks in a context are completed.
278
-
279
- Useful for suggesting context completion to user.
280
-
281
- Args:
282
- context_id: Context identifier
283
- project_root: Project root directory (default: cwd)
284
-
285
- Returns:
286
- True if all tasks are completed (or no tasks exist)
287
- """
288
- state = get_current_state(context_id, project_root)
289
-
290
- if not state.tasks:
291
- return True # No tasks = trivially complete
292
-
293
- return all(task.status == "completed" for task in state.tasks)
294
-
295
-
296
- def get_pending_tasks(context_id: str, project_root: Path = None) -> List[Task]:
297
- """
298
- Get all non-completed tasks from a context.
299
-
300
- Args:
301
- context_id: Context identifier
302
- project_root: Project root directory (default: cwd)
303
-
304
- Returns:
305
- List of tasks that are not completed
306
- """
307
- state = get_current_state(context_id, project_root)
308
- return [t for t in state.tasks if t.status != "completed"]
@@ -1,101 +0,0 @@
1
- """Plan archive utilities for context management.
2
-
3
- Provides functions for archiving plans to context folders and
4
- managing plan lifecycle.
5
-
6
- Used by:
7
- - ExitPlanMode hook to archive approved plans
8
- - SessionStart to detect pending implementations
9
- """
10
- import hashlib
11
- import shutil
12
- from datetime import datetime
13
- from pathlib import Path
14
- from typing import Optional, Tuple
15
-
16
- from .context_manager import (
17
- Context,
18
- create_context,
19
- get_context,
20
- get_all_contexts,
21
- update_plan_status,
22
- )
23
- from .event_log import append_event, EVENT_PLAN_CREATED
24
- from ..base.atomic_write import atomic_write
25
- from ..base.constants import get_context_plans_dir
26
- from ..base.utils import eprint, now_iso, sanitize_title
27
-
28
-
29
- def archive_plan_to_context(
30
- plan_path: str,
31
- context_id: str,
32
- project_root: Path = None
33
- ) -> Tuple[Optional[str], Optional[str]]:
34
- """
35
- Archive plan to context's plans folder.
36
-
37
- Actions:
38
- 1. Copy plan to _output/contexts/<context_id>/plans/<date>-<slug>.md
39
- 2. Compute plan hash for change detection
40
- 3. Update context.json: in_flight.mode = "pending_implementation"
41
- 4. Update context.json: in_flight.artifact_path = archived path
42
-
43
- Args:
44
- plan_path: Path to the plan file to archive
45
- context_id: Target context ID
46
- project_root: Project root directory
47
-
48
- Returns:
49
- Tuple of (archived_path, plan_hash) or (None, None) on error
50
- """
51
- plan_file = Path(plan_path)
52
- if not plan_file.exists():
53
- eprint(f"[plan_archive] Plan file not found: {plan_path}")
54
- return None, None
55
-
56
- # Read plan content
57
- try:
58
- plan_content = plan_file.read_text(encoding='utf-8')
59
- except Exception as e:
60
- eprint(f"[plan_archive] Failed to read plan: {e}")
61
- return None, None
62
-
63
- # Compute hash for change detection
64
- plan_hash = hashlib.sha256(plan_content.encode('utf-8')).hexdigest()[:12]
65
-
66
- # Create plans directory
67
- plans_dir = get_context_plans_dir(context_id, project_root)
68
- plans_dir.mkdir(parents=True, exist_ok=True)
69
-
70
- # Generate archive filename: YYYY-MM-DD-<slug>.md
71
- date_str = datetime.now().strftime("%Y-%m-%d")
72
- slug = sanitize_title(plan_file.stem, max_len=30)
73
- archive_name = f"{date_str}-{slug}.md"
74
- archive_path = plans_dir / archive_name
75
-
76
- # Handle name collision
77
- counter = 2
78
- while archive_path.exists():
79
- archive_name = f"{date_str}-{slug}-{counter}.md"
80
- archive_path = plans_dir / archive_name
81
- counter += 1
82
-
83
- # Write archived plan
84
- success, error = atomic_write(archive_path, plan_content)
85
- if not success:
86
- eprint(f"[plan_archive] Failed to write archive: {error}")
87
- return None, None
88
-
89
- # Update context plan status
90
- update_plan_status(
91
- context_id,
92
- status="pending_implementation",
93
- path=str(archive_path),
94
- hash=plan_hash,
95
- project_root=project_root
96
- )
97
-
98
- eprint(f"[plan_archive] Archived plan to: {archive_path}")
99
- return str(archive_path), plan_hash
100
-
101
-
@@ -1,290 +0,0 @@
1
- """Task synchronization utilities for Claude native task integration.
2
-
3
- Provides persistence for Claude Code native tasks:
4
- - Claude Code native TaskCreate/TaskUpdate/TaskList tools (ephemeral)
5
- - Persistent events.jsonl storage (source of truth)
6
-
7
- DURING SESSION (Persist):
8
- 1. Claude uses native TaskCreate/TaskUpdate
9
- 2. PostToolUse hooks capture events to events.jsonl
10
- 3. Task state preserved for future reference
11
-
12
- SESSION END:
13
- - events.jsonl has complete task history
14
- - Can be queried for context summaries
15
- """
16
- from pathlib import Path
17
- from typing import List, Optional
18
-
19
- from .event_log import (
20
- get_current_state,
21
- get_pending_tasks,
22
- append_event,
23
- Task,
24
- EVENT_TASK_ADDED,
25
- EVENT_TASK_STARTED,
26
- EVENT_TASK_COMPLETED,
27
- EVENT_TASK_BLOCKED,
28
- EVENT_SESSION_STARTED,
29
- )
30
- from ..base.utils import eprint
31
-
32
-
33
- def generate_task_summary(context_id: str, project_root: Path = None) -> str:
34
- """
35
- Generate a summary of all tasks in a context.
36
-
37
- Useful for status checks and completion suggestions.
38
-
39
- Args:
40
- context_id: Context identifier
41
- project_root: Project root directory
42
-
43
- Returns:
44
- Formatted task summary
45
- """
46
- state = get_current_state(context_id, project_root)
47
-
48
- if not state.tasks:
49
- return "No tasks in this context."
50
-
51
- completed = [t for t in state.tasks if t.status == "completed"]
52
- pending = [t for t in state.tasks if t.status == "pending"]
53
- in_progress = [t for t in state.tasks if t.status == "in_progress"]
54
- blocked = [t for t in state.tasks if t.status == "blocked"]
55
-
56
- lines = [
57
- f"## Task Summary for: {context_id}",
58
- "",
59
- f"**Total:** {len(state.tasks)} tasks",
60
- f"**Completed:** {len(completed)} | **In Progress:** {len(in_progress)} | **Pending:** {len(pending)} | **Blocked:** {len(blocked)}",
61
- "",
62
- ]
63
-
64
- if completed:
65
- lines.append("### Completed")
66
- for t in completed:
67
- lines.append(f"- [x] {t.subject}")
68
- lines.append("")
69
-
70
- if in_progress:
71
- lines.append("### In Progress")
72
- for t in in_progress:
73
- lines.append(f"- [~] {t.subject}")
74
- lines.append("")
75
-
76
- if pending:
77
- lines.append("### Pending")
78
- for t in pending:
79
- lines.append(f"- [ ] {t.subject}")
80
- lines.append("")
81
-
82
- if blocked:
83
- lines.append("### Blocked")
84
- for t in blocked:
85
- lines.append(f"- [!] {t.subject}: {t.blocked_reason}")
86
- lines.append("")
87
-
88
- return "\n".join(lines)
89
-
90
-
91
- def record_session_start(
92
- context_id: str,
93
- tasks_hydrated: Optional[List[str]] = None,
94
- project_root: Path = None
95
- ) -> bool:
96
- """
97
- Record a session_started event in the context's event log.
98
-
99
- Called after SessionStart hook loads a context.
100
-
101
- Args:
102
- context_id: Context identifier
103
- tasks_hydrated: List of task IDs that were restored
104
- project_root: Project root directory
105
-
106
- Returns:
107
- True if event was recorded successfully
108
- """
109
- event_data = {}
110
- if tasks_hydrated:
111
- event_data["tasks_hydrated"] = tasks_hydrated
112
-
113
- return append_event(
114
- context_id,
115
- EVENT_SESSION_STARTED,
116
- project_root,
117
- **event_data
118
- )
119
-
120
-
121
- def record_task_created(
122
- context_id: str,
123
- task_id: str,
124
- subject: str,
125
- description: str = "",
126
- active_form: str = "",
127
- project_root: Path = None
128
- ) -> bool:
129
- """
130
- Record a task_added event in the context's event log.
131
-
132
- Called when Claude creates a new task via TaskCreate.
133
-
134
- Args:
135
- context_id: Context identifier
136
- task_id: Persistent task ID (e.g., "aiw-1")
137
- subject: Task subject (required)
138
- description: Task description (optional)
139
- active_form: Spinner text for in_progress status (optional)
140
- project_root: Project root directory
141
-
142
- Returns:
143
- True if event was recorded successfully
144
- """
145
- event_data = {
146
- "task_id": task_id,
147
- "subject": subject,
148
- }
149
- if description:
150
- event_data["description"] = description
151
- if active_form:
152
- event_data["activeForm"] = active_form
153
-
154
- return append_event(
155
- context_id,
156
- EVENT_TASK_ADDED,
157
- project_root,
158
- **event_data
159
- )
160
-
161
-
162
- def record_task_started(
163
- context_id: str,
164
- task_id: str,
165
- project_root: Path = None
166
- ) -> bool:
167
- """
168
- Record a task_started event in the context's event log.
169
-
170
- Called when Claude starts working on a task.
171
-
172
- Args:
173
- context_id: Context identifier
174
- task_id: Persistent task ID
175
- project_root: Project root directory
176
-
177
- Returns:
178
- True if event was recorded successfully
179
- """
180
- return append_event(
181
- context_id,
182
- EVENT_TASK_STARTED,
183
- project_root,
184
- task_id=task_id
185
- )
186
-
187
-
188
- def record_task_completed(
189
- context_id: str,
190
- task_id: str,
191
- evidence: str,
192
- work_summary: str = "",
193
- files_changed: Optional[List[str]] = None,
194
- commit_ref: str = "",
195
- project_root: Path = None
196
- ) -> bool:
197
- """
198
- Record a task_completed event in the context's event log.
199
-
200
- Called when Claude completes a task.
201
-
202
- Args:
203
- context_id: Context identifier
204
- task_id: Persistent task ID
205
- evidence: Verification evidence (required)
206
- work_summary: Summary of work done (optional)
207
- files_changed: List of files modified (optional)
208
- commit_ref: Git commit reference (optional)
209
- project_root: Project root directory
210
-
211
- Returns:
212
- True if event was recorded successfully
213
- """
214
- event_data = {
215
- "task_id": task_id,
216
- "evidence": evidence,
217
- }
218
- if work_summary:
219
- event_data["work_summary"] = work_summary
220
- if files_changed:
221
- event_data["files_changed"] = files_changed
222
- if commit_ref:
223
- event_data["commit_ref"] = commit_ref
224
-
225
- return append_event(
226
- context_id,
227
- EVENT_TASK_COMPLETED,
228
- project_root,
229
- **event_data
230
- )
231
-
232
-
233
- def record_task_blocked(
234
- context_id: str,
235
- task_id: str,
236
- reason: str,
237
- project_root: Path = None
238
- ) -> bool:
239
- """
240
- Record a task_blocked event in the context's event log.
241
-
242
- Called when a task becomes blocked.
243
-
244
- Args:
245
- context_id: Context identifier
246
- task_id: Persistent task ID
247
- reason: Reason for being blocked
248
- project_root: Project root directory
249
-
250
- Returns:
251
- True if event was recorded successfully
252
- """
253
- return append_event(
254
- context_id,
255
- EVENT_TASK_BLOCKED,
256
- project_root,
257
- task_id=task_id,
258
- reason=reason
259
- )
260
-
261
-
262
- def generate_next_task_id(context_id: str, project_root: Path = None) -> str:
263
- """
264
- Generate the next sequential task ID for a context.
265
-
266
- Task IDs follow the pattern: aiw-{n} where n starts at 1.
267
-
268
- Args:
269
- context_id: Context identifier
270
- project_root: Project root directory
271
-
272
- Returns:
273
- Next available task ID (e.g., "aiw-3")
274
- """
275
- state = get_current_state(context_id, project_root)
276
-
277
- if not state.tasks:
278
- return "aiw-1"
279
-
280
- # Find highest existing task number
281
- max_num = 0
282
- for task in state.tasks:
283
- if task.id.startswith("aiw-"):
284
- try:
285
- num = int(task.id.split("-")[1])
286
- max_num = max(max_num, num)
287
- except (IndexError, ValueError):
288
- pass
289
-
290
- return f"aiw-{max_num + 1}"