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.
- package/dist/templates/CLAUDE.md +49 -18
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +4 -4
- package/dist/templates/_shared/hooks/context_monitor.py +78 -1
- package/dist/templates/_shared/hooks/pre_compact.py +89 -0
- package/dist/templates/_shared/hooks/session_end.py +111 -0
- package/dist/templates/_shared/hooks/session_start.py +104 -47
- package/dist/templates/_shared/hooks/task_create_atomicity.py +33 -61
- package/dist/templates/_shared/hooks/task_create_capture.py +1 -0
- package/dist/templates/_shared/hooks/task_update_capture.py +15 -0
- package/dist/templates/_shared/hooks/user_prompt_submit.py +13 -27
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/constants.py +18 -4
- package/dist/templates/_shared/lib/base/utils.py +9 -4
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/auto_state.py +167 -0
- package/dist/templates/_shared/lib/context/context_manager.py +6 -3
- package/dist/templates/_shared/lib/context/discovery.py +167 -57
- package/dist/templates/_shared/lib/context/event_log.py +8 -0
- package/dist/templates/_shared/lib/context/task_sync.py +160 -43
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +24 -41
- package/dist/templates/cc-native/.claude/settings.json +23 -1
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +8 -1
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +65 -47
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +29 -6
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +71 -15
- package/dist/templates/cc-native/_cc-native/lib/utils.py +3 -3
- package/oclif.manifest.json +1 -1
- 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
|
|
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
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
lines.append(
|
|
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.
|
|
169
|
-
"2.
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
lines.append(
|
|
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
|
|
218
|
-
"2.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
lines.append("
|
|
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
|
-
|
|
77
|
-
lines.append("
|
|
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
|
-
|
|
83
|
-
lines.append("
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
|
283
|
-
if
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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}"
|