aiwcli 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +3 -3
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.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_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +104 -247
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +47 -32
- package/dist/templates/_shared/hooks/session_end.py +103 -60
- package/dist/templates/_shared/hooks/session_start.py +110 -81
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
- package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +42 -9
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +41 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
- 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/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.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/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
- package/dist/templates/_shared/lib/context/auto_state.py +0 -167
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
- package/dist/templates/_shared/lib/context/discovery.py +0 -554
- package/dist/templates/_shared/lib/context/event_log.py +0 -316
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -407
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
|
@@ -1,407 +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
|
-
read_events,
|
|
24
|
-
Task,
|
|
25
|
-
EVENT_TASK_ADDED,
|
|
26
|
-
EVENT_TASK_STARTED,
|
|
27
|
-
EVENT_TASK_COMPLETED,
|
|
28
|
-
EVENT_TASK_BLOCKED,
|
|
29
|
-
EVENT_TASK_DELETED,
|
|
30
|
-
EVENT_SESSION_STARTED,
|
|
31
|
-
EVENT_SESSION_ENDED,
|
|
32
|
-
)
|
|
33
|
-
from ..base.utils import eprint
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def generate_task_summary(context_id: str, project_root: Path = None) -> str:
|
|
37
|
-
"""
|
|
38
|
-
Generate a session-aware summary of all tasks in a context.
|
|
39
|
-
|
|
40
|
-
Includes session boundary awareness: tasks left in_progress when a session
|
|
41
|
-
ended are marked as "interrupted" to distinguish from actively worked tasks.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
context_id: Context identifier
|
|
45
|
-
project_root: Project root directory
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
Formatted task summary with session context
|
|
49
|
-
"""
|
|
50
|
-
state = get_current_state(context_id, project_root)
|
|
51
|
-
|
|
52
|
-
if not state.tasks:
|
|
53
|
-
return "No tasks in this context."
|
|
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
|
-
|
|
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]
|
|
66
|
-
pending = [t for t in state.tasks if t.status == "pending"]
|
|
67
|
-
blocked = [t for t in state.tasks if t.status == "blocked"]
|
|
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
|
-
|
|
86
|
-
lines = [
|
|
87
|
-
f"### Previous Work ({len(state.tasks)} tasks{session_info})",
|
|
88
|
-
"",
|
|
89
|
-
]
|
|
90
|
-
|
|
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}")
|
|
96
|
-
|
|
97
|
-
for t in interrupted:
|
|
98
|
-
lines.append(f"- [~] {t.id}: {t.subject} (in progress when session ended)")
|
|
99
|
-
|
|
100
|
-
for t in in_progress:
|
|
101
|
-
lines.append(f"- [~] {t.id}: {t.subject}")
|
|
102
|
-
|
|
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}")
|
|
108
|
-
|
|
109
|
-
return "\n".join(lines)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def record_session_start(
|
|
113
|
-
context_id: str,
|
|
114
|
-
tasks_hydrated: Optional[List[str]] = None,
|
|
115
|
-
project_root: Path = None
|
|
116
|
-
) -> bool:
|
|
117
|
-
"""
|
|
118
|
-
Record a session_started event in the context's event log.
|
|
119
|
-
|
|
120
|
-
Called after SessionStart hook loads a context.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
context_id: Context identifier
|
|
124
|
-
tasks_hydrated: List of task IDs that were restored
|
|
125
|
-
project_root: Project root directory
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
True if event was recorded successfully
|
|
129
|
-
"""
|
|
130
|
-
event_data = {}
|
|
131
|
-
if tasks_hydrated:
|
|
132
|
-
event_data["tasks_hydrated"] = tasks_hydrated
|
|
133
|
-
|
|
134
|
-
return append_event(
|
|
135
|
-
context_id,
|
|
136
|
-
EVENT_SESSION_STARTED,
|
|
137
|
-
project_root,
|
|
138
|
-
**event_data
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def record_task_created(
|
|
143
|
-
context_id: str,
|
|
144
|
-
task_id: str,
|
|
145
|
-
subject: str,
|
|
146
|
-
description: str = "",
|
|
147
|
-
active_form: str = "",
|
|
148
|
-
session_id: str = "",
|
|
149
|
-
project_root: Path = None
|
|
150
|
-
) -> bool:
|
|
151
|
-
"""
|
|
152
|
-
Record a task_added event in the context's event log.
|
|
153
|
-
|
|
154
|
-
Called when Claude creates a new task via TaskCreate.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
context_id: Context identifier
|
|
158
|
-
task_id: Persistent task ID (e.g., "aiw-1")
|
|
159
|
-
subject: Task subject (required)
|
|
160
|
-
description: Task description (optional)
|
|
161
|
-
active_form: Spinner text for in_progress status (optional)
|
|
162
|
-
session_id: Session ID where task was created (optional)
|
|
163
|
-
project_root: Project root directory
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
True if event was recorded successfully
|
|
167
|
-
"""
|
|
168
|
-
event_data = {
|
|
169
|
-
"task_id": task_id,
|
|
170
|
-
"subject": subject,
|
|
171
|
-
}
|
|
172
|
-
if description:
|
|
173
|
-
event_data["description"] = description
|
|
174
|
-
if active_form:
|
|
175
|
-
event_data["activeForm"] = active_form
|
|
176
|
-
if session_id:
|
|
177
|
-
event_data["session_id"] = session_id
|
|
178
|
-
|
|
179
|
-
return append_event(
|
|
180
|
-
context_id,
|
|
181
|
-
EVENT_TASK_ADDED,
|
|
182
|
-
project_root,
|
|
183
|
-
**event_data
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def record_task_started(
|
|
188
|
-
context_id: str,
|
|
189
|
-
task_id: str,
|
|
190
|
-
session_id: str = "",
|
|
191
|
-
project_root: Path = None
|
|
192
|
-
) -> bool:
|
|
193
|
-
"""
|
|
194
|
-
Record a task_started event in the context's event log.
|
|
195
|
-
|
|
196
|
-
Called when Claude starts working on a task.
|
|
197
|
-
|
|
198
|
-
Args:
|
|
199
|
-
context_id: Context identifier
|
|
200
|
-
task_id: Persistent task ID
|
|
201
|
-
session_id: Session ID where task was started (optional)
|
|
202
|
-
project_root: Project root directory
|
|
203
|
-
|
|
204
|
-
Returns:
|
|
205
|
-
True if event was recorded successfully
|
|
206
|
-
"""
|
|
207
|
-
event_data = {"task_id": task_id}
|
|
208
|
-
if session_id:
|
|
209
|
-
event_data["session_id"] = session_id
|
|
210
|
-
|
|
211
|
-
return append_event(
|
|
212
|
-
context_id,
|
|
213
|
-
EVENT_TASK_STARTED,
|
|
214
|
-
project_root,
|
|
215
|
-
**event_data
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def record_task_completed(
|
|
220
|
-
context_id: str,
|
|
221
|
-
task_id: str,
|
|
222
|
-
evidence: str,
|
|
223
|
-
work_summary: str = "",
|
|
224
|
-
files_changed: Optional[List[str]] = None,
|
|
225
|
-
commit_ref: str = "",
|
|
226
|
-
session_id: str = "",
|
|
227
|
-
project_root: Path = None
|
|
228
|
-
) -> bool:
|
|
229
|
-
"""
|
|
230
|
-
Record a task_completed event in the context's event log.
|
|
231
|
-
|
|
232
|
-
Called when Claude completes a task.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
context_id: Context identifier
|
|
236
|
-
task_id: Persistent task ID
|
|
237
|
-
evidence: Verification evidence (required)
|
|
238
|
-
work_summary: Summary of work done (optional)
|
|
239
|
-
files_changed: List of files modified (optional)
|
|
240
|
-
commit_ref: Git commit reference (optional)
|
|
241
|
-
session_id: Session ID where task was completed (optional)
|
|
242
|
-
project_root: Project root directory
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
True if event was recorded successfully
|
|
246
|
-
"""
|
|
247
|
-
event_data = {
|
|
248
|
-
"task_id": task_id,
|
|
249
|
-
"evidence": evidence,
|
|
250
|
-
}
|
|
251
|
-
if work_summary:
|
|
252
|
-
event_data["work_summary"] = work_summary
|
|
253
|
-
if files_changed:
|
|
254
|
-
event_data["files_changed"] = files_changed
|
|
255
|
-
if commit_ref:
|
|
256
|
-
event_data["commit_ref"] = commit_ref
|
|
257
|
-
if session_id:
|
|
258
|
-
event_data["session_id"] = session_id
|
|
259
|
-
|
|
260
|
-
return append_event(
|
|
261
|
-
context_id,
|
|
262
|
-
EVENT_TASK_COMPLETED,
|
|
263
|
-
project_root,
|
|
264
|
-
**event_data
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def record_task_blocked(
|
|
269
|
-
context_id: str,
|
|
270
|
-
task_id: str,
|
|
271
|
-
reason: str,
|
|
272
|
-
session_id: str = "",
|
|
273
|
-
project_root: Path = None
|
|
274
|
-
) -> bool:
|
|
275
|
-
"""
|
|
276
|
-
Record a task_blocked event in the context's event log.
|
|
277
|
-
|
|
278
|
-
Called when a task becomes blocked.
|
|
279
|
-
|
|
280
|
-
Args:
|
|
281
|
-
context_id: Context identifier
|
|
282
|
-
task_id: Persistent task ID
|
|
283
|
-
reason: Reason for being blocked
|
|
284
|
-
session_id: Session ID where task was blocked (optional)
|
|
285
|
-
project_root: Project root directory
|
|
286
|
-
|
|
287
|
-
Returns:
|
|
288
|
-
True if event was recorded successfully
|
|
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
|
-
|
|
297
|
-
return append_event(
|
|
298
|
-
context_id,
|
|
299
|
-
EVENT_TASK_BLOCKED,
|
|
300
|
-
project_root,
|
|
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
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def generate_next_task_id(context_id: str, project_root: Path = None) -> str:
|
|
380
|
-
"""
|
|
381
|
-
Generate the next sequential task ID for a context.
|
|
382
|
-
|
|
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.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
context_id: Context identifier
|
|
388
|
-
project_root: Project root directory
|
|
389
|
-
|
|
390
|
-
Returns:
|
|
391
|
-
Next available task ID (e.g., "aiw-3")
|
|
392
|
-
"""
|
|
393
|
-
# Scan all events to find highest task ID ever used (including deleted)
|
|
394
|
-
events = read_events(context_id, project_root)
|
|
395
|
-
|
|
396
|
-
max_num = 0
|
|
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
|
|
406
|
-
|
|
407
|
-
return f"aiw-{max_num + 1}"
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"""Persona-based question templates for plan clarification.
|
|
2
|
-
|
|
3
|
-
Uses distinct reasoning lenses to surface hidden constraints and assumptions.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from typing import List, Dict
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class PersonaQuestion:
|
|
12
|
-
"""A clarifying question from a specific persona lens."""
|
|
13
|
-
|
|
14
|
-
persona: str
|
|
15
|
-
display_name: str
|
|
16
|
-
question: str
|
|
17
|
-
purpose: str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
CLARIFICATION_PERSONAS: Dict[str, List[PersonaQuestion]] = {
|
|
21
|
-
"problem_validator": [
|
|
22
|
-
PersonaQuestion(
|
|
23
|
-
persona="problem_validator",
|
|
24
|
-
display_name="Questioning the Problem",
|
|
25
|
-
question="Can you describe the problem you're trying to solve without mentioning the solution?",
|
|
26
|
-
purpose="Separates problem from solution to check alignment",
|
|
27
|
-
),
|
|
28
|
-
PersonaQuestion(
|
|
29
|
-
persona="problem_validator",
|
|
30
|
-
display_name="Challenging the Approach",
|
|
31
|
-
question="What's the simplest possible way to achieve this outcome that we haven't considered?",
|
|
32
|
-
purpose="Identifies potential over-engineering",
|
|
33
|
-
),
|
|
34
|
-
],
|
|
35
|
-
"assumption_validator": [
|
|
36
|
-
PersonaQuestion(
|
|
37
|
-
persona="assumption_validator",
|
|
38
|
-
display_name="Surfacing Assumptions",
|
|
39
|
-
question="What must already be true about your users, systems, or constraints for this to succeed?",
|
|
40
|
-
purpose="Surfaces foundational assumptions that could invalidate the plan",
|
|
41
|
-
),
|
|
42
|
-
PersonaQuestion(
|
|
43
|
-
persona="assumption_validator",
|
|
44
|
-
display_name="Hidden Dependencies",
|
|
45
|
-
question="What are you assuming 'everyone knows' about this problem that might not be documented?",
|
|
46
|
-
purpose="Uncovers implicit knowledge that needs to be made explicit",
|
|
47
|
-
),
|
|
48
|
-
],
|
|
49
|
-
"user_advocate": [
|
|
50
|
-
PersonaQuestion(
|
|
51
|
-
persona="user_advocate",
|
|
52
|
-
display_name="Understanding Users",
|
|
53
|
-
question="Who specifically will use this, and what problem does it solve for them today?",
|
|
54
|
-
purpose="Grounds the plan in actual user needs",
|
|
55
|
-
),
|
|
56
|
-
PersonaQuestion(
|
|
57
|
-
persona="user_advocate",
|
|
58
|
-
display_name="Impact Assessment",
|
|
59
|
-
question="If we did nothing, what would happen? Who would be affected?",
|
|
60
|
-
purpose="Establishes urgency and stakes",
|
|
61
|
-
),
|
|
62
|
-
],
|
|
63
|
-
"tradeoff_illuminator": [
|
|
64
|
-
PersonaQuestion(
|
|
65
|
-
persona="tradeoff_illuminator",
|
|
66
|
-
display_name="Revealing Trade-offs",
|
|
67
|
-
question="What are you willing to sacrifice (scope, time, quality, features) to make this work?",
|
|
68
|
-
purpose="Forces explicit prioritization",
|
|
69
|
-
),
|
|
70
|
-
PersonaQuestion(
|
|
71
|
-
persona="tradeoff_illuminator",
|
|
72
|
-
display_name="Foreclosed Options",
|
|
73
|
-
question="What becomes harder or impossible to do later if we proceed this way?",
|
|
74
|
-
purpose="Surfaces opportunity costs and lock-in risks",
|
|
75
|
-
),
|
|
76
|
-
],
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def get_all_persona_questions() -> List[PersonaQuestion]:
|
|
81
|
-
"""Get all persona questions as a flat list."""
|
|
82
|
-
questions = []
|
|
83
|
-
for persona_qs in CLARIFICATION_PERSONAS.values():
|
|
84
|
-
questions.extend(persona_qs)
|
|
85
|
-
return questions
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def format_questions_for_prompt() -> str:
|
|
89
|
-
"""Format persona questions for injection into Claude prompt."""
|
|
90
|
-
lines = [
|
|
91
|
-
"### Persona-Based Clarifying Questions",
|
|
92
|
-
"",
|
|
93
|
-
"Ask 5-8 questions from these perspectives using AskUserQuestion:",
|
|
94
|
-
"",
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
for persona_qs in CLARIFICATION_PERSONAS.values():
|
|
98
|
-
for q in persona_qs:
|
|
99
|
-
lines.append(f"**{q.display_name}**")
|
|
100
|
-
lines.append(f'- Q: "{q.question}"')
|
|
101
|
-
lines.append(f"- Purpose: {q.purpose}")
|
|
102
|
-
lines.append("")
|
|
103
|
-
|
|
104
|
-
lines.extend(
|
|
105
|
-
[
|
|
106
|
-
"**Guidance:**",
|
|
107
|
-
"- Select questions most relevant to THIS plan (skip if already answered)",
|
|
108
|
-
"- Ask one at a time with clear context",
|
|
109
|
-
"- Use answers to refine the plan before ExitPlanMode",
|
|
110
|
-
]
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
return "\n".join(lines)
|
|
Binary file
|
package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""Async background archival to avoid blocking user workflow."""
|
|
2
|
-
import threading
|
|
3
|
-
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Dict, Any, Callable, Optional
|
|
6
|
-
try:
|
|
7
|
-
from .atomic_write import atomic_write
|
|
8
|
-
from .constants import ENABLE_ROBUST_PLAN_WRITES
|
|
9
|
-
except ImportError:
|
|
10
|
-
# When imported directly via sys.path (not as a package)
|
|
11
|
-
from atomic_write import atomic_write
|
|
12
|
-
from constants import ENABLE_ROBUST_PLAN_WRITES
|
|
13
|
-
|
|
14
|
-
def archive_plan_async(
|
|
15
|
-
out_path: Path,
|
|
16
|
-
header: str,
|
|
17
|
-
plan: str,
|
|
18
|
-
callback: Optional[Callable] = None
|
|
19
|
-
) -> None:
|
|
20
|
-
"""
|
|
21
|
-
Archive plan in background thread. Non-blocking.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
out_path: Destination file path
|
|
25
|
-
header: Plan header with metadata
|
|
26
|
-
plan: Plan content
|
|
27
|
-
callback: Optional callback(success: bool, error: str) on completion
|
|
28
|
-
"""
|
|
29
|
-
if not ENABLE_ROBUST_PLAN_WRITES:
|
|
30
|
-
# Legacy behavior - write directly
|
|
31
|
-
try:
|
|
32
|
-
out_path.write_text(header + plan + "\n", encoding="utf-8")
|
|
33
|
-
if callback:
|
|
34
|
-
callback(True, None)
|
|
35
|
-
except Exception as e:
|
|
36
|
-
if callback:
|
|
37
|
-
callback(False, str(e))
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
def _archive_worker():
|
|
41
|
-
success, error = atomic_write(out_path, header + plan + "\n")
|
|
42
|
-
|
|
43
|
-
if not success:
|
|
44
|
-
# Write sanitized error marker (no stack traces)
|
|
45
|
-
error_marker = out_path.with_suffix('.error')
|
|
46
|
-
error_content = f"Archive failed: {error}\n"
|
|
47
|
-
|
|
48
|
-
try:
|
|
49
|
-
# Use atomic write for error marker too
|
|
50
|
-
atomic_write(
|
|
51
|
-
error_marker,
|
|
52
|
-
error_content,
|
|
53
|
-
max_attempts=1 # Don't retry error marker
|
|
54
|
-
)
|
|
55
|
-
except Exception:
|
|
56
|
-
pass # Error marker is best-effort
|
|
57
|
-
|
|
58
|
-
if callback:
|
|
59
|
-
try:
|
|
60
|
-
callback(success, error)
|
|
61
|
-
except Exception as e:
|
|
62
|
-
# Log callback failures (daemon thread would otherwise swallow)
|
|
63
|
-
import sys
|
|
64
|
-
print(f"[async_archive] Callback failed: {e}", file=sys.stderr)
|
|
65
|
-
|
|
66
|
-
# Start background thread
|
|
67
|
-
thread = threading.Thread(target=_archive_worker, daemon=False)
|
|
68
|
-
thread.start()
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"""Cross-platform atomic file writes with security."""
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
import tempfile
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
if sys.platform == 'win32':
|
|
9
|
-
import ctypes
|
|
10
|
-
from ctypes import wintypes
|
|
11
|
-
|
|
12
|
-
# Windows MoveFileEx flags
|
|
13
|
-
MOVEFILE_REPLACE_EXISTING = 0x1
|
|
14
|
-
MOVEFILE_WRITE_THROUGH = 0x8
|
|
15
|
-
|
|
16
|
-
def _atomic_replace_windows(src: Path, dst: Path) -> None:
|
|
17
|
-
"""Atomic file replacement on Windows using MoveFileEx."""
|
|
18
|
-
kernel32 = ctypes.windll.kernel32
|
|
19
|
-
|
|
20
|
-
# Set proper function prototypes for 64-bit safety
|
|
21
|
-
kernel32.MoveFileExW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD]
|
|
22
|
-
kernel32.MoveFileExW.restype = wintypes.BOOL
|
|
23
|
-
|
|
24
|
-
result = kernel32.MoveFileExW(
|
|
25
|
-
str(src),
|
|
26
|
-
str(dst),
|
|
27
|
-
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH
|
|
28
|
-
)
|
|
29
|
-
if not result:
|
|
30
|
-
error_code = kernel32.GetLastError()
|
|
31
|
-
# Use ctypes.WinError for human-readable error messages
|
|
32
|
-
raise ctypes.WinError(error_code)
|
|
33
|
-
|
|
34
|
-
def atomic_write(
|
|
35
|
-
path: Path,
|
|
36
|
-
content: str,
|
|
37
|
-
max_attempts: int = 2,
|
|
38
|
-
backoff_ms: list = None
|
|
39
|
-
) -> tuple:
|
|
40
|
-
"""
|
|
41
|
-
Write file atomically with retry logic.
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
(success: bool, error_message: Optional[str])
|
|
45
|
-
"""
|
|
46
|
-
import time
|
|
47
|
-
|
|
48
|
-
if backoff_ms is None:
|
|
49
|
-
backoff_ms = [500, 1000]
|
|
50
|
-
|
|
51
|
-
for attempt in range(max_attempts):
|
|
52
|
-
try:
|
|
53
|
-
# Create temp file in same directory for atomic rename
|
|
54
|
-
temp_fd, temp_path_str = tempfile.mkstemp(
|
|
55
|
-
dir=path.parent,
|
|
56
|
-
prefix=f".{path.stem}_",
|
|
57
|
-
suffix=".tmp"
|
|
58
|
-
)
|
|
59
|
-
temp_path = Path(temp_path_str)
|
|
60
|
-
|
|
61
|
-
try:
|
|
62
|
-
# Write content to temp file
|
|
63
|
-
with os.fdopen(temp_fd, 'w', encoding='utf-8') as f:
|
|
64
|
-
f.write(content)
|
|
65
|
-
f.flush()
|
|
66
|
-
os.fsync(f.fileno()) # Force write to disk
|
|
67
|
-
|
|
68
|
-
# Set restrictive permissions before rename (chmod 600)
|
|
69
|
-
os.chmod(temp_path, 0o600)
|
|
70
|
-
|
|
71
|
-
# Platform-specific atomic rename
|
|
72
|
-
if sys.platform == 'win32':
|
|
73
|
-
_atomic_replace_windows(temp_path, path)
|
|
74
|
-
else:
|
|
75
|
-
temp_path.replace(path) # POSIX atomic
|
|
76
|
-
|
|
77
|
-
return (True, None)
|
|
78
|
-
|
|
79
|
-
except Exception as e:
|
|
80
|
-
# Clean up temp file on failure
|
|
81
|
-
try:
|
|
82
|
-
temp_path.unlink()
|
|
83
|
-
except Exception:
|
|
84
|
-
pass # Cleanup is best-effort
|
|
85
|
-
raise
|
|
86
|
-
|
|
87
|
-
except Exception as e:
|
|
88
|
-
if attempt < max_attempts - 1:
|
|
89
|
-
# Bounds-safe backoff indexing
|
|
90
|
-
wait_ms = backoff_ms[min(attempt, len(backoff_ms) - 1)]
|
|
91
|
-
time.sleep(wait_ms / 1000.0)
|
|
92
|
-
else:
|
|
93
|
-
# Sanitize error message (no paths, no stack trace)
|
|
94
|
-
error_type = type(e).__name__
|
|
95
|
-
error_msg = str(e).split('\n')[0][:200] # First line only, max 200 chars
|
|
96
|
-
return (False, f"{error_type}: {error_msg}")
|
|
97
|
-
|
|
98
|
-
return (False, "Max retry attempts exceeded")
|