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.
Files changed (116) 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 +3 -3
  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_capture.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  17. package/dist/templates/_shared/hooks/context_monitor.py +104 -247
  18. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  19. package/dist/templates/_shared/hooks/pre_compact.py +47 -32
  20. package/dist/templates/_shared/hooks/session_end.py +103 -60
  21. package/dist/templates/_shared/hooks/session_start.py +110 -81
  22. package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
  23. package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
  24. package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
  25. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  32. package/dist/templates/_shared/lib/base/inference.py +121 -0
  33. package/dist/templates/_shared/lib/base/logger.py +291 -0
  34. package/dist/templates/_shared/lib/base/utils.py +42 -9
  35. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  36. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  47. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  48. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  49. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  51. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  52. package/dist/templates/_shared/lib/templates/README.md +5 -13
  53. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  54. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  56. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  57. package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
  58. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  61. package/dist/templates/_shared/scripts/status_line.py +701 -0
  62. package/dist/templates/_shared/workflows/handoff.md +9 -3
  63. package/dist/templates/cc-native/.claude/settings.json +41 -8
  64. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  65. package/dist/templates/cc-native/MIGRATION.md +1 -1
  66. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
  68. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  71. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
  75. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
  76. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  79. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  87. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  96. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  97. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  98. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  99. package/oclif.manifest.json +1 -1
  100. package/package.json +1 -1
  101. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  102. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  103. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  104. package/dist/templates/_shared/lib/context/cache.py +0 -444
  105. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  106. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  107. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  108. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  109. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  110. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  111. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  115. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  116. 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)
@@ -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")