aiwcli 0.9.7 → 0.9.8

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 (40) hide show
  1. package/dist/templates/CLAUDE.md +49 -18
  2. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  3. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  4. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  5. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/context_enforcer.py +4 -4
  7. package/dist/templates/_shared/hooks/context_monitor.py +78 -1
  8. package/dist/templates/_shared/hooks/pre_compact.py +89 -0
  9. package/dist/templates/_shared/hooks/session_end.py +111 -0
  10. package/dist/templates/_shared/hooks/session_start.py +104 -47
  11. package/dist/templates/_shared/hooks/task_create_atomicity.py +33 -61
  12. package/dist/templates/_shared/hooks/task_create_capture.py +1 -0
  13. package/dist/templates/_shared/hooks/task_update_capture.py +15 -0
  14. package/dist/templates/_shared/hooks/user_prompt_submit.py +13 -27
  15. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/lib/base/constants.py +18 -4
  18. package/dist/templates/_shared/lib/base/utils.py +9 -4
  19. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/context/auto_state.py +167 -0
  21. package/dist/templates/_shared/lib/context/context_manager.py +6 -3
  22. package/dist/templates/_shared/lib/context/discovery.py +167 -57
  23. package/dist/templates/_shared/lib/context/event_log.py +8 -0
  24. package/dist/templates/_shared/lib/context/task_sync.py +160 -43
  25. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  26. package/dist/templates/_shared/lib/templates/plan_context.py +24 -41
  27. package/dist/templates/cc-native/.claude/settings.json +23 -1
  28. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  29. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +8 -1
  30. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  31. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  32. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -2
  33. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +65 -47
  34. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +29 -6
  35. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  36. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  37. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +71 -15
  38. package/dist/templates/cc-native/_cc-native/lib/utils.py +3 -3
  39. package/oclif.manifest.json +1 -1
  40. package/package.json +1 -1
@@ -19,10 +19,110 @@ from .context_manager import (
19
19
  get_context_with_in_flight_work,
20
20
  )
21
21
  from .event_log import get_current_state, get_pending_tasks, Task
22
- from ..base.utils import parse_iso_timestamp
22
+ from .auto_state import load_auto_state
23
+ from .task_sync import generate_task_summary
24
+ from ..base.utils import eprint, parse_iso_timestamp
25
+ from ..base.constants import get_context_dir
23
26
  from ..templates.formatters import get_status_icon, format_continuation_header, get_mode_display
24
27
 
25
28
 
29
+ def find_plan_path(context: Context, project_root: Path = None) -> Optional[str]:
30
+ """
31
+ Find the most relevant plan path for a context.
32
+
33
+ Priority:
34
+ 1. Active plan (in_flight.artifact_path) if file exists
35
+ 2. Most recent archived plan by mtime
36
+ 3. None if no plans found
37
+
38
+ Args:
39
+ context: Context to find plan for
40
+ project_root: Project root directory
41
+
42
+ Returns:
43
+ Plan file path string or None
44
+ """
45
+ # 1. Active plan (in_flight.artifact_path)
46
+ if context.in_flight and context.in_flight.artifact_path:
47
+ plan_path = Path(context.in_flight.artifact_path)
48
+ if plan_path.exists():
49
+ return str(plan_path)
50
+
51
+ # 2. Archived plans (most recent by mtime)
52
+ plans_dir = get_context_dir(context.id, project_root) / "plans"
53
+ if plans_dir.exists():
54
+ plans = sorted(plans_dir.glob("*.md"), key=lambda p: p.stat().st_mtime, reverse=True)
55
+ if plans:
56
+ return str(plans[0])
57
+
58
+ # 3. No plan found
59
+ return None
60
+
61
+
62
+ def _build_restore_sections(
63
+ context: Context,
64
+ project_root: Path = None
65
+ ) -> str:
66
+ """
67
+ Build restoration context sections from auto-state and task history.
68
+
69
+ Used by formatters to inject richer context when resuming work.
70
+ Returns empty string if no restoration data is available (fresh context).
71
+
72
+ Args:
73
+ context: Context being restored
74
+ project_root: Project root directory
75
+
76
+ Returns:
77
+ Formatted markdown sections (may be empty)
78
+ """
79
+ sections = []
80
+
81
+ # Load auto-state for git info and session end metadata
82
+ auto_state = load_auto_state(context.id, project_root)
83
+
84
+ # Add session end info if available
85
+ if auto_state:
86
+ saved_at = auto_state.get("saved_at", "")
87
+ save_reason = auto_state.get("save_reason", "")
88
+ if saved_at:
89
+ time_str = format_relative_time(saved_at)
90
+ reason_display = save_reason.replace("_", " ") if save_reason else "unknown"
91
+ sections.append(f"**Last session ended:** {time_str} ({reason_display})")
92
+
93
+ # Task summary (session-aware)
94
+ task_summary = generate_task_summary(context.id, project_root)
95
+ if task_summary and task_summary != "No tasks in this context.":
96
+ sections.append("")
97
+ sections.append(task_summary)
98
+
99
+ # Plan path
100
+ plan_path = find_plan_path(context, project_root)
101
+ if plan_path:
102
+ sections.append("")
103
+ sections.append("### Plan")
104
+ sections.append(f"Read the plan at: `{plan_path}`")
105
+
106
+ # Git state from auto-state
107
+ if auto_state:
108
+ git_state = auto_state.get("git_state", {})
109
+ if git_state:
110
+ branch = git_state.get("branch", "unknown")
111
+ uncommitted = git_state.get("uncommitted_files", [])
112
+ last_commit = git_state.get("last_commit_short", "")
113
+
114
+ sections.append("")
115
+ sections.append("### Git State")
116
+ uncommitted_str = ", ".join(uncommitted[:5]) if uncommitted else "none"
117
+ if len(uncommitted) > 5:
118
+ uncommitted_str += f" (+{len(uncommitted) - 5} more)"
119
+ sections.append(f"Branch: {branch} | Uncommitted: {uncommitted_str}")
120
+ if last_commit:
121
+ sections.append(f"Last commit: {last_commit}")
122
+
123
+ return "\n".join(sections)
124
+
125
+
26
126
  def discover_contexts_for_session(
27
127
  project_root: Path = None
28
128
  ) -> Tuple[List[Context], Optional[Context]]:
@@ -125,7 +225,7 @@ def format_context_list(contexts: List[Context]) -> str:
125
225
  return "\n".join(lines)
126
226
 
127
227
 
128
- def format_pending_plan_continuation(context: Context) -> str:
228
+ def format_pending_plan_continuation(context: Context, project_root: Path = None) -> str:
129
229
  """
130
230
  Format output for plan handoff scenario.
131
231
 
@@ -135,47 +235,36 @@ def format_pending_plan_continuation(context: Context) -> str:
135
235
 
136
236
  Args:
137
237
  context: Context with pending plan implementation
238
+ project_root: Project root directory
138
239
 
139
240
  Returns:
140
241
  Formatted instructions for Claude
141
242
  """
142
243
  lines = [
143
- format_continuation_header("context", context.id),
244
+ f"## Resuming Context: {context.id}",
144
245
  "",
145
246
  f"**Summary:** {context.summary}",
146
- "",
247
+ f"**Mode:** Pending Implementation",
147
248
  ]
148
249
 
149
- # Add plan info
150
- if context.in_flight and context.in_flight.artifact_path:
151
- lines.append(f"**Plan pending implementation:**")
152
- lines.append(f"`{context.in_flight.artifact_path}`")
153
- lines.append("")
154
-
155
- # Add pending tasks if any
156
- tasks = get_pending_tasks(context.id)
157
- if tasks:
158
- lines.append("**Previous tasks:**")
159
- for task in tasks:
160
- status_icon = get_status_icon(task.status)
161
- lines.append(f" {status_icon} {task.subject}")
162
- lines.append("")
250
+ # Add restore sections (auto-state, tasks, git)
251
+ restore = _build_restore_sections(context, project_root)
252
+ if restore:
253
+ lines.append(restore)
163
254
 
164
255
  lines.extend([
256
+ "",
165
257
  "---",
166
258
  "",
167
259
  "**Instructions:**",
168
- "1. Read the plan file above",
169
- "2. Use TaskCreate to restore any pending tasks from the plan",
170
- "3. Begin implementing the approved plan",
171
- "",
172
- "The context has been loaded. You may begin implementation.",
260
+ "1. Review the plan and previous work above",
261
+ "2. Continue from where the previous session left off",
173
262
  ])
174
263
 
175
264
  return "\n".join(lines)
176
265
 
177
266
 
178
- def format_implementation_continuation(context: Context) -> str:
267
+ def format_implementation_continuation(context: Context, project_root: Path = None) -> str:
179
268
  """
180
269
  Format output for ongoing implementation scenario.
181
270
 
@@ -184,41 +273,30 @@ def format_implementation_continuation(context: Context) -> str:
184
273
 
185
274
  Args:
186
275
  context: Context with implementation in progress
276
+ project_root: Project root directory
187
277
 
188
278
  Returns:
189
279
  Formatted instructions for Claude
190
280
  """
191
281
  lines = [
192
- format_continuation_header("implementing", context.id),
282
+ f"## Resuming Context: {context.id}",
193
283
  "",
194
284
  f"**Summary:** {context.summary}",
195
- "",
285
+ f"**Mode:** Implementing",
196
286
  ]
197
287
 
198
- # Add plan info
199
- if context.in_flight and context.in_flight.artifact_path:
200
- lines.append(f"**Plan being implemented:**")
201
- lines.append(f"`{context.in_flight.artifact_path}`")
202
- lines.append("")
203
-
204
- # Add pending tasks
205
- tasks = get_pending_tasks(context.id)
206
- if tasks:
207
- lines.append("**Pending tasks:**")
208
- for task in tasks:
209
- status_icon = get_status_icon(task.status)
210
- lines.append(f" {status_icon} {task.subject}")
211
- lines.append("")
288
+ # Add restore sections (auto-state, tasks, git)
289
+ restore = _build_restore_sections(context, project_root)
290
+ if restore:
291
+ lines.append(restore)
212
292
 
213
293
  lines.extend([
294
+ "",
214
295
  "---",
215
296
  "",
216
297
  "**Instructions:**",
217
- "1. Review the plan and pending tasks above",
218
- "2. Use TaskCreate to restore pending tasks",
219
- "3. Continue implementing",
220
- "",
221
- "The context has been loaded. You may continue.",
298
+ "1. Review the plan and previous work above",
299
+ "2. Continue from where the previous session left off",
222
300
  ])
223
301
 
224
302
  return "\n".join(lines)
@@ -333,14 +411,23 @@ def format_context_selection_required(contexts: List[Context]) -> str:
333
411
  return "\n".join(lines)
334
412
 
335
413
 
336
- def format_active_context_reminder(context: Context) -> str:
414
+ def format_active_context_reminder(
415
+ context: Context,
416
+ project_root: Path = None,
417
+ include_restore: bool = False
418
+ ) -> str:
337
419
  """
338
420
  Format system reminder for active context.
339
421
 
340
- Used by context enforcer hook to inject context awareness.
422
+ Called in two situations:
423
+ 1. session_match (every prompt): include_restore=False → lightweight
424
+ 2. auto_selected first bind: include_restore=True → rich restore context
341
425
 
342
426
  Args:
343
427
  context: Active context
428
+ project_root: Project root directory
429
+ include_restore: If True, include auto-state/tasks/git restore sections.
430
+ Only set True on first bind to avoid per-prompt overhead.
344
431
 
345
432
  Returns:
346
433
  Formatted system reminder
@@ -356,16 +443,39 @@ def format_active_context_reminder(context: Context) -> str:
356
443
  # Remove brackets from "[Planning]" to get "Planning"
357
444
  mode_display = mode_str.strip("[]")
358
445
 
359
- lines = [
360
- f"## Active Context: {context.id}",
361
- "",
362
- f"**Summary:** {context.summary}",
363
- f"**Mode:** {mode_display}",
364
- f"**Last Active:** {time_str}",
365
- "",
366
- f'All work belongs to context "{context.id}".',
367
- "Tasks created with TaskCreate will be persisted to this context.",
368
- ]
446
+ if include_restore:
447
+ # Rich restore: first bind to existing context in new session
448
+ lines = [
449
+ f"## Resuming Context: {context.id}",
450
+ "",
451
+ f"**Summary:** {context.summary}",
452
+ f"**Mode:** {mode_display}",
453
+ ]
454
+
455
+ restore = _build_restore_sections(context, project_root)
456
+ if restore:
457
+ lines.append(restore)
458
+
459
+ lines.extend([
460
+ "",
461
+ "---",
462
+ "",
463
+ "**Instructions:**",
464
+ "1. Review the previous work above",
465
+ "2. Continue from where the previous session left off",
466
+ ])
467
+ else:
468
+ # Lightweight: subsequent prompts in same session
469
+ lines = [
470
+ f"## Active Context: {context.id}",
471
+ "",
472
+ f"**Summary:** {context.summary}",
473
+ f"**Mode:** {mode_display}",
474
+ f"**Last Active:** {time_str}",
475
+ "",
476
+ f'All work belongs to context "{context.id}".',
477
+ "Tasks created with TaskCreate will be persisted to this context.",
478
+ ]
369
479
 
370
480
  return "\n".join(lines)
371
481
 
@@ -41,6 +41,9 @@ EVENT_PLAN_IMPLEMENTATION_STARTED = "plan_implementation_started"
41
41
  EVENT_PLAN_COMPLETED = "plan_completed"
42
42
  EVENT_HANDOFF_CREATED = "handoff_created"
43
43
  EVENT_HANDOFF_CLEARED = "handoff_cleared"
44
+ EVENT_SESSION_ENDED = "session_ended"
45
+ EVENT_AUTO_STATE_SAVED = "auto_state_saved"
46
+ EVENT_TASK_DELETED = "task_deleted"
44
47
 
45
48
 
46
49
  @dataclass
@@ -248,6 +251,11 @@ def get_current_state(context_id: str, project_root: Path = None) -> ContextStat
248
251
  tasks_map[task_id].status = "blocked"
249
252
  tasks_map[task_id].blocked_reason = event.get("reason", "")
250
253
 
254
+ elif event_type == EVENT_TASK_DELETED:
255
+ task_id = event.get("task_id")
256
+ if task_id and task_id in tasks_map:
257
+ del tasks_map[task_id]
258
+
251
259
  elif event_type == EVENT_NOTE_ADDED:
252
260
  note = event.get("content", "")
253
261
  if note:
@@ -20,70 +20,91 @@ from .event_log import (
20
20
  get_current_state,
21
21
  get_pending_tasks,
22
22
  append_event,
23
+ read_events,
23
24
  Task,
24
25
  EVENT_TASK_ADDED,
25
26
  EVENT_TASK_STARTED,
26
27
  EVENT_TASK_COMPLETED,
27
28
  EVENT_TASK_BLOCKED,
29
+ EVENT_TASK_DELETED,
28
30
  EVENT_SESSION_STARTED,
31
+ EVENT_SESSION_ENDED,
29
32
  )
30
33
  from ..base.utils import eprint
31
34
 
32
35
 
33
36
  def generate_task_summary(context_id: str, project_root: Path = None) -> str:
34
37
  """
35
- Generate a summary of all tasks in a context.
38
+ Generate a session-aware summary of all tasks in a context.
36
39
 
37
- Useful for status checks and completion suggestions.
40
+ Includes session boundary awareness: tasks left in_progress when a session
41
+ ended are marked as "interrupted" to distinguish from actively worked tasks.
38
42
 
39
43
  Args:
40
44
  context_id: Context identifier
41
45
  project_root: Project root directory
42
46
 
43
47
  Returns:
44
- Formatted task summary
48
+ Formatted task summary with session context
45
49
  """
46
50
  state = get_current_state(context_id, project_root)
47
51
 
48
52
  if not state.tasks:
49
53
  return "No tasks in this context."
50
54
 
55
+ # Find the latest session_ended event to detect interrupted tasks
56
+ events = read_events(context_id, project_root)
57
+ interrupted_task_ids = set()
58
+ for event in reversed(events):
59
+ if event.get("event") == EVENT_SESSION_ENDED:
60
+ interrupted_task_ids = set(event.get("active_tasks", []))
61
+ break
62
+
51
63
  completed = [t for t in state.tasks if t.status == "completed"]
64
+ interrupted = [t for t in state.tasks if t.status == "in_progress" and t.id in interrupted_task_ids]
65
+ in_progress = [t for t in state.tasks if t.status == "in_progress" and t.id not in interrupted_task_ids]
52
66
  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
67
  blocked = [t for t in state.tasks if t.status == "blocked"]
55
68
 
69
+ # Count sessions from session_ended events
70
+ session_count = sum(1 for e in events if e.get("event") == EVENT_SESSION_ENDED)
71
+
72
+ parts = []
73
+ if completed:
74
+ parts.append(f"{len(completed)} completed")
75
+ if interrupted:
76
+ parts.append(f"{len(interrupted)} interrupted")
77
+ if in_progress:
78
+ parts.append(f"{len(in_progress)} in progress")
79
+ if pending:
80
+ parts.append(f"{len(pending)} pending")
81
+ if blocked:
82
+ parts.append(f"{len(blocked)} blocked")
83
+
84
+ session_info = f" across {session_count} session{'s' if session_count != 1 else ''}" if session_count > 0 else ""
85
+
56
86
  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)}",
87
+ f"### Previous Work ({len(state.tasks)} tasks{session_info})",
61
88
  "",
62
89
  ]
63
90
 
64
- if completed:
65
- lines.append("### Completed")
66
- for t in completed:
67
- lines.append(f"- [x] {t.subject}")
68
- lines.append("")
91
+ for t in completed:
92
+ work_info = ""
93
+ if t.work_summary:
94
+ work_info = f"\n Work: {t.work_summary}"
95
+ lines.append(f"- [x] {t.id}: {t.subject}{work_info}")
69
96
 
70
- if in_progress:
71
- lines.append("### In Progress")
72
- for t in in_progress:
73
- lines.append(f"- [~] {t.subject}")
74
- lines.append("")
97
+ for t in interrupted:
98
+ lines.append(f"- [~] {t.id}: {t.subject} (in progress when session ended)")
75
99
 
76
- if pending:
77
- lines.append("### Pending")
78
- for t in pending:
79
- lines.append(f"- [ ] {t.subject}")
80
- lines.append("")
100
+ for t in in_progress:
101
+ lines.append(f"- [~] {t.id}: {t.subject}")
81
102
 
82
- if blocked:
83
- lines.append("### Blocked")
84
- for t in blocked:
85
- lines.append(f"- [!] {t.subject}: {t.blocked_reason}")
86
- lines.append("")
103
+ for t in pending:
104
+ lines.append(f"- [ ] {t.id}: {t.subject}")
105
+
106
+ for t in blocked:
107
+ lines.append(f"- [!] {t.id}: {t.subject}: {t.blocked_reason}")
87
108
 
88
109
  return "\n".join(lines)
89
110
 
@@ -124,6 +145,7 @@ def record_task_created(
124
145
  subject: str,
125
146
  description: str = "",
126
147
  active_form: str = "",
148
+ session_id: str = "",
127
149
  project_root: Path = None
128
150
  ) -> bool:
129
151
  """
@@ -137,6 +159,7 @@ def record_task_created(
137
159
  subject: Task subject (required)
138
160
  description: Task description (optional)
139
161
  active_form: Spinner text for in_progress status (optional)
162
+ session_id: Session ID where task was created (optional)
140
163
  project_root: Project root directory
141
164
 
142
165
  Returns:
@@ -150,6 +173,8 @@ def record_task_created(
150
173
  event_data["description"] = description
151
174
  if active_form:
152
175
  event_data["activeForm"] = active_form
176
+ if session_id:
177
+ event_data["session_id"] = session_id
153
178
 
154
179
  return append_event(
155
180
  context_id,
@@ -162,6 +187,7 @@ def record_task_created(
162
187
  def record_task_started(
163
188
  context_id: str,
164
189
  task_id: str,
190
+ session_id: str = "",
165
191
  project_root: Path = None
166
192
  ) -> bool:
167
193
  """
@@ -172,16 +198,21 @@ def record_task_started(
172
198
  Args:
173
199
  context_id: Context identifier
174
200
  task_id: Persistent task ID
201
+ session_id: Session ID where task was started (optional)
175
202
  project_root: Project root directory
176
203
 
177
204
  Returns:
178
205
  True if event was recorded successfully
179
206
  """
207
+ event_data = {"task_id": task_id}
208
+ if session_id:
209
+ event_data["session_id"] = session_id
210
+
180
211
  return append_event(
181
212
  context_id,
182
213
  EVENT_TASK_STARTED,
183
214
  project_root,
184
- task_id=task_id
215
+ **event_data
185
216
  )
186
217
 
187
218
 
@@ -192,6 +223,7 @@ def record_task_completed(
192
223
  work_summary: str = "",
193
224
  files_changed: Optional[List[str]] = None,
194
225
  commit_ref: str = "",
226
+ session_id: str = "",
195
227
  project_root: Path = None
196
228
  ) -> bool:
197
229
  """
@@ -206,6 +238,7 @@ def record_task_completed(
206
238
  work_summary: Summary of work done (optional)
207
239
  files_changed: List of files modified (optional)
208
240
  commit_ref: Git commit reference (optional)
241
+ session_id: Session ID where task was completed (optional)
209
242
  project_root: Project root directory
210
243
 
211
244
  Returns:
@@ -221,6 +254,8 @@ def record_task_completed(
221
254
  event_data["files_changed"] = files_changed
222
255
  if commit_ref:
223
256
  event_data["commit_ref"] = commit_ref
257
+ if session_id:
258
+ event_data["session_id"] = session_id
224
259
 
225
260
  return append_event(
226
261
  context_id,
@@ -234,6 +269,7 @@ def record_task_blocked(
234
269
  context_id: str,
235
270
  task_id: str,
236
271
  reason: str,
272
+ session_id: str = "",
237
273
  project_root: Path = None
238
274
  ) -> bool:
239
275
  """
@@ -245,17 +281,98 @@ def record_task_blocked(
245
281
  context_id: Context identifier
246
282
  task_id: Persistent task ID
247
283
  reason: Reason for being blocked
284
+ session_id: Session ID where task was blocked (optional)
248
285
  project_root: Project root directory
249
286
 
250
287
  Returns:
251
288
  True if event was recorded successfully
252
289
  """
290
+ event_data = {
291
+ "task_id": task_id,
292
+ "reason": reason,
293
+ }
294
+ if session_id:
295
+ event_data["session_id"] = session_id
296
+
253
297
  return append_event(
254
298
  context_id,
255
299
  EVENT_TASK_BLOCKED,
256
300
  project_root,
257
- task_id=task_id,
258
- reason=reason
301
+ **event_data
302
+ )
303
+
304
+
305
+ def record_task_deleted(
306
+ context_id: str,
307
+ task_id: str,
308
+ session_id: str = "",
309
+ project_root: Path = None
310
+ ) -> bool:
311
+ """
312
+ Record a task_deleted event in the context's event log.
313
+
314
+ Called when Claude deletes a task via TaskUpdate with status="deleted".
315
+
316
+ Args:
317
+ context_id: Context identifier
318
+ task_id: Persistent task ID
319
+ session_id: Session ID where task was deleted (optional)
320
+ project_root: Project root directory
321
+
322
+ Returns:
323
+ True if event was recorded successfully
324
+ """
325
+ event_data = {"task_id": task_id}
326
+ if session_id:
327
+ event_data["session_id"] = session_id
328
+
329
+ return append_event(
330
+ context_id,
331
+ EVENT_TASK_DELETED,
332
+ project_root,
333
+ **event_data
334
+ )
335
+
336
+
337
+ def record_session_ended(
338
+ context_id: str,
339
+ session_id: str,
340
+ reason: str = "other",
341
+ active_tasks: Optional[List[str]] = None,
342
+ pending_tasks: Optional[List[str]] = None,
343
+ project_root: Path = None
344
+ ) -> bool:
345
+ """
346
+ Record a session_ended event in the context's event log.
347
+
348
+ Creates a session boundary marker. Tasks left in_progress at session end
349
+ are recorded so they can be identified as "interrupted" during restore.
350
+
351
+ Args:
352
+ context_id: Context identifier
353
+ session_id: Session ID that ended
354
+ reason: Why session ended (prompt_input_exit, clear, logout, other)
355
+ active_tasks: Task IDs that were in_progress at session end
356
+ pending_tasks: Task IDs still pending at session end
357
+ project_root: Project root directory
358
+
359
+ Returns:
360
+ True if event was recorded successfully
361
+ """
362
+ event_data = {
363
+ "session_id": session_id,
364
+ "reason": reason,
365
+ }
366
+ if active_tasks:
367
+ event_data["active_tasks"] = active_tasks
368
+ if pending_tasks:
369
+ event_data["pending_tasks"] = pending_tasks
370
+
371
+ return append_event(
372
+ context_id,
373
+ EVENT_SESSION_ENDED,
374
+ project_root,
375
+ **event_data
259
376
  )
260
377
 
261
378
 
@@ -264,6 +381,7 @@ def generate_next_task_id(context_id: str, project_root: Path = None) -> str:
264
381
  Generate the next sequential task ID for a context.
265
382
 
266
383
  Task IDs follow the pattern: aiw-{n} where n starts at 1.
384
+ Accounts for deleted tasks by scanning all events, not just current state.
267
385
 
268
386
  Args:
269
387
  context_id: Context identifier
@@ -272,19 +390,18 @@ def generate_next_task_id(context_id: str, project_root: Path = None) -> str:
272
390
  Returns:
273
391
  Next available task ID (e.g., "aiw-3")
274
392
  """
275
- state = get_current_state(context_id, project_root)
276
-
277
- if not state.tasks:
278
- return "aiw-1"
393
+ # Scan all events to find highest task ID ever used (including deleted)
394
+ events = read_events(context_id, project_root)
279
395
 
280
- # Find highest existing task number
281
396
  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
397
+ for event in events:
398
+ if event.get("event") == EVENT_TASK_ADDED:
399
+ task_id = event.get("task_id", "")
400
+ if task_id.startswith("aiw-"):
401
+ try:
402
+ num = int(task_id.split("-")[1])
403
+ max_num = max(max_num, num)
404
+ except (IndexError, ValueError):
405
+ pass
289
406
 
290
407
  return f"aiw-{max_num + 1}"