aiwcli 0.9.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 (204) hide show
  1. package/README.md +1248 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +16 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +19 -0
  6. package/dist/commands/branch.d.ts +45 -0
  7. package/dist/commands/branch.js +488 -0
  8. package/dist/commands/clean.d.ts +34 -0
  9. package/dist/commands/clean.js +186 -0
  10. package/dist/commands/clear.d.ts +51 -0
  11. package/dist/commands/clear.js +835 -0
  12. package/dist/commands/init/index.d.ts +107 -0
  13. package/dist/commands/init/index.js +565 -0
  14. package/dist/commands/launch.d.ts +21 -0
  15. package/dist/commands/launch.js +108 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/lib/base-command.d.ts +114 -0
  19. package/dist/lib/base-command.js +153 -0
  20. package/dist/lib/bmad-installer.d.ts +38 -0
  21. package/dist/lib/bmad-installer.js +145 -0
  22. package/dist/lib/claude-settings-types.d.ts +102 -0
  23. package/dist/lib/claude-settings-types.js +5 -0
  24. package/dist/lib/config.d.ts +25 -0
  25. package/dist/lib/config.js +46 -0
  26. package/dist/lib/debug.d.ts +39 -0
  27. package/dist/lib/debug.js +74 -0
  28. package/dist/lib/env-compat.d.ts +26 -0
  29. package/dist/lib/env-compat.js +35 -0
  30. package/dist/lib/errors.d.ts +126 -0
  31. package/dist/lib/errors.js +145 -0
  32. package/dist/lib/generic-merge.d.ts +74 -0
  33. package/dist/lib/generic-merge.js +105 -0
  34. package/dist/lib/git/branch.d.ts +67 -0
  35. package/dist/lib/git/branch.js +155 -0
  36. package/dist/lib/git/index.d.ts +11 -0
  37. package/dist/lib/git/index.js +13 -0
  38. package/dist/lib/git/safety-checks.d.ts +44 -0
  39. package/dist/lib/git/safety-checks.js +102 -0
  40. package/dist/lib/git/types.d.ts +31 -0
  41. package/dist/lib/git/types.js +6 -0
  42. package/dist/lib/git/worktree.d.ts +67 -0
  43. package/dist/lib/git/worktree.js +220 -0
  44. package/dist/lib/gitignore-manager.d.ts +10 -0
  45. package/dist/lib/gitignore-manager.js +60 -0
  46. package/dist/lib/hooks-merger.d.ts +28 -0
  47. package/dist/lib/hooks-merger.js +94 -0
  48. package/dist/lib/ide-path-resolver.d.ts +102 -0
  49. package/dist/lib/ide-path-resolver.js +129 -0
  50. package/dist/lib/index.d.ts +13 -0
  51. package/dist/lib/index.js +22 -0
  52. package/dist/lib/output.d.ts +51 -0
  53. package/dist/lib/output.js +76 -0
  54. package/dist/lib/paths.d.ts +66 -0
  55. package/dist/lib/paths.js +136 -0
  56. package/dist/lib/quiet.d.ts +12 -0
  57. package/dist/lib/quiet.js +17 -0
  58. package/dist/lib/settings-hierarchy.d.ts +42 -0
  59. package/dist/lib/settings-hierarchy.js +105 -0
  60. package/dist/lib/spawn.d.ts +105 -0
  61. package/dist/lib/spawn.js +157 -0
  62. package/dist/lib/spinner.d.ts +19 -0
  63. package/dist/lib/spinner.js +34 -0
  64. package/dist/lib/stdin.d.ts +48 -0
  65. package/dist/lib/stdin.js +60 -0
  66. package/dist/lib/template-installer.d.ts +92 -0
  67. package/dist/lib/template-installer.js +375 -0
  68. package/dist/lib/template-linter.d.ts +49 -0
  69. package/dist/lib/template-linter.js +173 -0
  70. package/dist/lib/template-merger.d.ts +47 -0
  71. package/dist/lib/template-merger.js +173 -0
  72. package/dist/lib/template-resolver.d.ts +20 -0
  73. package/dist/lib/template-resolver.js +60 -0
  74. package/dist/lib/terminal.d.ts +102 -0
  75. package/dist/lib/terminal.js +245 -0
  76. package/dist/lib/tty-detection.d.ts +62 -0
  77. package/dist/lib/tty-detection.js +83 -0
  78. package/dist/lib/user-utils.d.ts +5 -0
  79. package/dist/lib/user-utils.js +23 -0
  80. package/dist/lib/version.d.ts +99 -0
  81. package/dist/lib/version.js +144 -0
  82. package/dist/lib/watch-templates.d.ts +6 -0
  83. package/dist/lib/watch-templates.js +73 -0
  84. package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
  85. package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
  86. package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
  87. package/dist/lib/windsurf-hooks-merger.js +53 -0
  88. package/dist/lib/windsurf-hooks-types.d.ts +33 -0
  89. package/dist/lib/windsurf-hooks-types.js +5 -0
  90. package/dist/templates/CLAUDE.md +174 -0
  91. package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
  92. package/dist/templates/_shared/.claude/settings.json +61 -0
  93. package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
  94. package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
  95. package/dist/templates/_shared/hooks/__init__.py +16 -0
  96. package/dist/templates/_shared/hooks/archive_plan.py +270 -0
  97. package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
  98. package/dist/templates/_shared/hooks/context_monitor.py +322 -0
  99. package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
  100. package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
  101. package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
  102. package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
  103. package/dist/templates/_shared/lib/__init__.py +1 -0
  104. package/dist/templates/_shared/lib/base/__init__.py +49 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
  107. package/dist/templates/_shared/lib/base/constants.py +299 -0
  108. package/dist/templates/_shared/lib/base/inference.py +189 -0
  109. package/dist/templates/_shared/lib/base/utils.py +216 -0
  110. package/dist/templates/_shared/lib/context/__init__.py +119 -0
  111. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  112. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  113. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  114. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  115. package/dist/templates/_shared/lib/context/cache.py +446 -0
  116. package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
  117. package/dist/templates/_shared/lib/context/discovery.py +486 -0
  118. package/dist/templates/_shared/lib/context/event_log.py +308 -0
  119. package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
  120. package/dist/templates/_shared/lib/context/task_sync.py +367 -0
  121. package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
  122. package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
  123. package/dist/templates/_shared/lib/templates/README.md +215 -0
  124. package/dist/templates/_shared/lib/templates/__init__.py +40 -0
  125. package/dist/templates/_shared/lib/templates/formatters.py +147 -0
  126. package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
  127. package/dist/templates/_shared/scripts/save_handoff.py +99 -0
  128. package/dist/templates/_shared/workflows/handoff.md +212 -0
  129. package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
  130. package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
  131. package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
  132. package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
  133. package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
  134. package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
  135. package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
  136. package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
  137. package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
  138. package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
  139. package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
  140. package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
  141. package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
  142. package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
  143. package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
  144. package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
  145. package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
  146. package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
  147. package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
  148. package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
  149. package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
  150. package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
  151. package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
  152. package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
  153. package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
  154. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
  155. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
  156. package/dist/templates/cc-native/.claude/settings.json +119 -0
  157. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
  158. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
  159. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
  160. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
  161. package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
  162. package/dist/templates/cc-native/MIGRATION.md +86 -0
  163. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
  164. package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
  165. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +150 -0
  171. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
  172. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
  174. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  175. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  176. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +68 -0
  179. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
  180. package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
  181. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  187. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +164 -0
  189. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
  190. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
  191. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
  192. package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
  193. package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
  194. package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
  195. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  196. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
  197. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
  198. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
  199. package/dist/types/exit-codes.d.ts +11 -0
  200. package/dist/types/exit-codes.js +10 -0
  201. package/dist/types/index.d.ts +5 -0
  202. package/dist/types/index.js +7 -0
  203. package/oclif.manifest.json +405 -0
  204. package/package.json +109 -0
@@ -0,0 +1,367 @@
1
+ """Task synchronization utilities for Claude native task integration.
2
+
3
+ Provides bi-directional sync between:
4
+ - Claude Code native TaskCreate/TaskUpdate/TaskList tools (ephemeral)
5
+ - Persistent events.jsonl storage (source of truth)
6
+
7
+ SESSION START (Hydrate):
8
+ 1. Read events.jsonl -> compute pending tasks
9
+ 2. Output instructions for Claude to recreate tasks via TaskCreate
10
+ 3. Claude's native TaskList now populated with persistent state
11
+
12
+ DURING SESSION (Persist):
13
+ 1. Claude uses native TaskCreate/TaskUpdate
14
+ 2. CLAUDE.md instructs: after TaskUpdate, call append_event()
15
+ 3. Both systems stay in sync
16
+
17
+ SESSION END:
18
+ - events.jsonl already has everything
19
+ - Next session will hydrate from it
20
+ """
21
+ from pathlib import Path
22
+ from typing import List, Optional
23
+
24
+ from .event_log import (
25
+ get_current_state,
26
+ get_pending_tasks,
27
+ append_event,
28
+ Task,
29
+ EVENT_TASK_ADDED,
30
+ EVENT_TASK_STARTED,
31
+ EVENT_TASK_COMPLETED,
32
+ EVENT_TASK_BLOCKED,
33
+ EVENT_SESSION_STARTED,
34
+ )
35
+ from ..base.utils import eprint
36
+
37
+
38
+ def _escape_string(s: str) -> str:
39
+ """
40
+ Escape a string for safe embedding in quoted YAML-like format.
41
+
42
+ Handles backslashes, newlines, tabs, and quotes.
43
+
44
+ Args:
45
+ s: Input string
46
+
47
+ Returns:
48
+ Escaped string safe for embedding in double quotes
49
+ """
50
+ if not s:
51
+ return ""
52
+ # Order matters: escape backslashes first, then other special chars
53
+ s = s.replace('\\', '\\\\')
54
+ s = s.replace('\n', '\\n')
55
+ s = s.replace('\r', '\\r')
56
+ s = s.replace('\t', '\\t')
57
+ s = s.replace('"', '\\"')
58
+ return s
59
+
60
+
61
+ def generate_hydration_instructions(
62
+ context_id: str,
63
+ project_root: Path = None
64
+ ) -> str:
65
+ """
66
+ Generate instructions for Claude to recreate tasks from persistent storage.
67
+
68
+ Called by SessionStart hook when resuming a context.
69
+
70
+ Args:
71
+ context_id: Context identifier
72
+ project_root: Project root directory
73
+
74
+ Returns:
75
+ Formatted instructions for Claude to restore tasks
76
+ """
77
+ pending_tasks = get_pending_tasks(context_id, project_root)
78
+
79
+ if not pending_tasks:
80
+ return "No pending tasks to restore."
81
+
82
+ lines = [
83
+ "## Restoring Tasks from Previous Session",
84
+ "",
85
+ "Please recreate these tasks using TaskCreate:",
86
+ "",
87
+ ]
88
+
89
+ for task in pending_tasks:
90
+ lines.append(f"### Task: {task.subject}")
91
+ lines.append("")
92
+ lines.append("```")
93
+ lines.append("TaskCreate:")
94
+ # Escape special characters for YAML-like format
95
+ subject_escaped = _escape_string(task.subject)
96
+ lines.append(f' subject: "{subject_escaped}"')
97
+ if task.description:
98
+ desc_escaped = _escape_string(task.description)
99
+ lines.append(f' description: "{desc_escaped}"')
100
+ if task.active_form:
101
+ active_form_escaped = _escape_string(task.active_form)
102
+ lines.append(f' activeForm: "{active_form_escaped}"')
103
+ lines.append(f' metadata: {{"persistent_id": "{task.id}", "context": "{context_id}", "skip_persistence": true}}')
104
+ lines.append("```")
105
+ lines.append("")
106
+
107
+ return "\n".join(lines)
108
+
109
+
110
+ def generate_task_summary(context_id: str, project_root: Path = None) -> str:
111
+ """
112
+ Generate a summary of all tasks in a context.
113
+
114
+ Useful for status checks and completion suggestions.
115
+
116
+ Args:
117
+ context_id: Context identifier
118
+ project_root: Project root directory
119
+
120
+ Returns:
121
+ Formatted task summary
122
+ """
123
+ state = get_current_state(context_id, project_root)
124
+
125
+ if not state.tasks:
126
+ return "No tasks in this context."
127
+
128
+ completed = [t for t in state.tasks if t.status == "completed"]
129
+ pending = [t for t in state.tasks if t.status == "pending"]
130
+ in_progress = [t for t in state.tasks if t.status == "in_progress"]
131
+ blocked = [t for t in state.tasks if t.status == "blocked"]
132
+
133
+ lines = [
134
+ f"## Task Summary for: {context_id}",
135
+ "",
136
+ f"**Total:** {len(state.tasks)} tasks",
137
+ f"**Completed:** {len(completed)} | **In Progress:** {len(in_progress)} | **Pending:** {len(pending)} | **Blocked:** {len(blocked)}",
138
+ "",
139
+ ]
140
+
141
+ if completed:
142
+ lines.append("### Completed")
143
+ for t in completed:
144
+ lines.append(f"- [x] {t.subject}")
145
+ lines.append("")
146
+
147
+ if in_progress:
148
+ lines.append("### In Progress")
149
+ for t in in_progress:
150
+ lines.append(f"- [~] {t.subject}")
151
+ lines.append("")
152
+
153
+ if pending:
154
+ lines.append("### Pending")
155
+ for t in pending:
156
+ lines.append(f"- [ ] {t.subject}")
157
+ lines.append("")
158
+
159
+ if blocked:
160
+ lines.append("### Blocked")
161
+ for t in blocked:
162
+ lines.append(f"- [!] {t.subject}: {t.blocked_reason}")
163
+ lines.append("")
164
+
165
+ return "\n".join(lines)
166
+
167
+
168
+ def record_session_start(
169
+ context_id: str,
170
+ tasks_hydrated: Optional[List[str]] = None,
171
+ project_root: Path = None
172
+ ) -> bool:
173
+ """
174
+ Record a session_started event in the context's event log.
175
+
176
+ Called after SessionStart hook loads a context.
177
+
178
+ Args:
179
+ context_id: Context identifier
180
+ tasks_hydrated: List of task IDs that were restored
181
+ project_root: Project root directory
182
+
183
+ Returns:
184
+ True if event was recorded successfully
185
+ """
186
+ event_data = {}
187
+ if tasks_hydrated:
188
+ event_data["tasks_hydrated"] = tasks_hydrated
189
+
190
+ return append_event(
191
+ context_id,
192
+ EVENT_SESSION_STARTED,
193
+ project_root,
194
+ **event_data
195
+ )
196
+
197
+
198
+ def record_task_created(
199
+ context_id: str,
200
+ task_id: str,
201
+ subject: str,
202
+ description: str = "",
203
+ active_form: str = "",
204
+ project_root: Path = None
205
+ ) -> bool:
206
+ """
207
+ Record a task_added event in the context's event log.
208
+
209
+ Called when Claude creates a new task via TaskCreate.
210
+
211
+ Args:
212
+ context_id: Context identifier
213
+ task_id: Persistent task ID (e.g., "aiw-1")
214
+ subject: Task subject (required)
215
+ description: Task description (optional)
216
+ active_form: Spinner text for in_progress status (optional)
217
+ project_root: Project root directory
218
+
219
+ Returns:
220
+ True if event was recorded successfully
221
+ """
222
+ event_data = {
223
+ "task_id": task_id,
224
+ "subject": subject,
225
+ }
226
+ if description:
227
+ event_data["description"] = description
228
+ if active_form:
229
+ event_data["activeForm"] = active_form
230
+
231
+ return append_event(
232
+ context_id,
233
+ EVENT_TASK_ADDED,
234
+ project_root,
235
+ **event_data
236
+ )
237
+
238
+
239
+ def record_task_started(
240
+ context_id: str,
241
+ task_id: str,
242
+ project_root: Path = None
243
+ ) -> bool:
244
+ """
245
+ Record a task_started event in the context's event log.
246
+
247
+ Called when Claude starts working on a task.
248
+
249
+ Args:
250
+ context_id: Context identifier
251
+ task_id: Persistent task ID
252
+ project_root: Project root directory
253
+
254
+ Returns:
255
+ True if event was recorded successfully
256
+ """
257
+ return append_event(
258
+ context_id,
259
+ EVENT_TASK_STARTED,
260
+ project_root,
261
+ task_id=task_id
262
+ )
263
+
264
+
265
+ def record_task_completed(
266
+ context_id: str,
267
+ task_id: str,
268
+ evidence: str,
269
+ work_summary: str = "",
270
+ files_changed: Optional[List[str]] = None,
271
+ commit_ref: str = "",
272
+ project_root: Path = None
273
+ ) -> bool:
274
+ """
275
+ Record a task_completed event in the context's event log.
276
+
277
+ Called when Claude completes a task.
278
+
279
+ Args:
280
+ context_id: Context identifier
281
+ task_id: Persistent task ID
282
+ evidence: Verification evidence (required)
283
+ work_summary: Summary of work done (optional)
284
+ files_changed: List of files modified (optional)
285
+ commit_ref: Git commit reference (optional)
286
+ project_root: Project root directory
287
+
288
+ Returns:
289
+ True if event was recorded successfully
290
+ """
291
+ event_data = {
292
+ "task_id": task_id,
293
+ "evidence": evidence,
294
+ }
295
+ if work_summary:
296
+ event_data["work_summary"] = work_summary
297
+ if files_changed:
298
+ event_data["files_changed"] = files_changed
299
+ if commit_ref:
300
+ event_data["commit_ref"] = commit_ref
301
+
302
+ return append_event(
303
+ context_id,
304
+ EVENT_TASK_COMPLETED,
305
+ project_root,
306
+ **event_data
307
+ )
308
+
309
+
310
+ def record_task_blocked(
311
+ context_id: str,
312
+ task_id: str,
313
+ reason: str,
314
+ project_root: Path = None
315
+ ) -> bool:
316
+ """
317
+ Record a task_blocked event in the context's event log.
318
+
319
+ Called when a task becomes blocked.
320
+
321
+ Args:
322
+ context_id: Context identifier
323
+ task_id: Persistent task ID
324
+ reason: Reason for being blocked
325
+ project_root: Project root directory
326
+
327
+ Returns:
328
+ True if event was recorded successfully
329
+ """
330
+ return append_event(
331
+ context_id,
332
+ EVENT_TASK_BLOCKED,
333
+ project_root,
334
+ task_id=task_id,
335
+ reason=reason
336
+ )
337
+
338
+
339
+ def generate_next_task_id(context_id: str, project_root: Path = None) -> str:
340
+ """
341
+ Generate the next sequential task ID for a context.
342
+
343
+ Task IDs follow the pattern: aiw-{n} where n starts at 1.
344
+
345
+ Args:
346
+ context_id: Context identifier
347
+ project_root: Project root directory
348
+
349
+ Returns:
350
+ Next available task ID (e.g., "aiw-3")
351
+ """
352
+ state = get_current_state(context_id, project_root)
353
+
354
+ if not state.tasks:
355
+ return "aiw-1"
356
+
357
+ # Find highest existing task number
358
+ max_num = 0
359
+ for task in state.tasks:
360
+ if task.id.startswith("aiw-"):
361
+ try:
362
+ num = int(task.id.split("-")[1])
363
+ max_num = max(max_num, num)
364
+ except (IndexError, ValueError):
365
+ pass
366
+
367
+ return f"aiw-{max_num + 1}"
@@ -0,0 +1,22 @@
1
+ """Handoff utilities for context-aware session management.
2
+
3
+ This module provides graceful context degradation when Claude's
4
+ context window fills up. Instead of rushing or losing work,
5
+ it creates a handoff document and facilitates clean session continuation.
6
+
7
+ Components:
8
+ - document_generator: Creates handoff documents with work state
9
+ - context_monitor hook: Monitors context during tool use and triggers warnings
10
+ """
11
+
12
+ from .document_generator import (
13
+ generate_handoff_document,
14
+ get_handoff_continuation_prompt,
15
+ HandoffDocument,
16
+ )
17
+
18
+ __all__ = [
19
+ "generate_handoff_document",
20
+ "get_handoff_continuation_prompt",
21
+ "HandoffDocument",
22
+ ]
@@ -0,0 +1,307 @@
1
+ """Handoff document generator for context-aware session management.
2
+
3
+ Creates structured handoff documents when a session needs to transfer
4
+ work to a new session (typically due to context window limits).
5
+
6
+ Handoff documents capture:
7
+ - Links to active plan and context folder
8
+ - Current task state from events.jsonl
9
+ - Work in progress summary
10
+ - Next steps for continuation
11
+ """
12
+ import json
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from ..base.atomic_write import atomic_write
20
+ from ..base.constants import get_context_handoffs_dir, get_context_dir
21
+ from ..base.utils import eprint, now_iso
22
+ from ..context.event_log import (
23
+ append_event,
24
+ get_current_state,
25
+ get_pending_tasks,
26
+ Task,
27
+ EVENT_HANDOFF_CREATED,
28
+ )
29
+ from ..templates.formatters import render_task_list, format_continuation_header, format_reason
30
+
31
+
32
+ @dataclass
33
+ class HandoffDocument:
34
+ """Structured handoff document content."""
35
+ context_id: str
36
+ context_summary: str
37
+ session_id: str
38
+ reason: str # e.g., "low_context", "user_requested", "error_recovery"
39
+ created_at: str
40
+
41
+ # Links
42
+ plan_path: Optional[str] = None
43
+ context_folder: str = ""
44
+ events_log_path: str = ""
45
+
46
+ # Task state
47
+ active_tasks: List[Dict[str, Any]] = field(default_factory=list)
48
+ completed_tasks_this_session: List[Dict[str, Any]] = field(default_factory=list)
49
+
50
+ # Context summary
51
+ work_summary: str = ""
52
+ next_steps: List[str] = field(default_factory=list)
53
+ important_notes: List[str] = field(default_factory=list)
54
+
55
+ # File path (set after saving)
56
+ file_path: Optional[str] = None
57
+
58
+
59
+ def generate_handoff_document(
60
+ context_id: str,
61
+ reason: str = "low_context",
62
+ work_summary: str = "",
63
+ next_steps: Optional[List[str]] = None,
64
+ important_notes: Optional[List[str]] = None,
65
+ completed_this_session: Optional[List[str]] = None,
66
+ project_root: Path = None
67
+ ) -> Optional[HandoffDocument]:
68
+ """
69
+ Generate and save a handoff document for a context.
70
+
71
+ This creates a markdown document capturing current work state,
72
+ saves it to the context's handoffs folder, and records the event.
73
+
74
+ Args:
75
+ context_id: Context identifier
76
+ reason: Why handoff is happening (low_context, user_requested, etc.)
77
+ work_summary: Summary of current work in progress
78
+ next_steps: List of next steps for continuation
79
+ important_notes: Important decisions or context to preserve
80
+ completed_this_session: List of task subjects completed this session
81
+ project_root: Project root directory
82
+
83
+ Returns:
84
+ HandoffDocument with file_path set, or None on failure
85
+ """
86
+ from ..context.context_manager import get_context, update_handoff_status
87
+
88
+ context = get_context(context_id, project_root)
89
+ if not context:
90
+ eprint(f"[handoff] ERROR: Context '{context_id}' not found")
91
+ return None
92
+
93
+ # Generate session ID
94
+ session_id = str(uuid.uuid4())[:8]
95
+
96
+ # Get current state
97
+ state = get_current_state(context_id, project_root)
98
+ pending_tasks = get_pending_tasks(context_id, project_root)
99
+
100
+ # Build document
101
+ now = now_iso()
102
+ context_dir = get_context_dir(context_id, project_root)
103
+
104
+ doc = HandoffDocument(
105
+ context_id=context_id,
106
+ context_summary=context.summary,
107
+ session_id=session_id,
108
+ reason=reason,
109
+ created_at=now,
110
+ plan_path=context.in_flight.artifact_path if context.in_flight else None,
111
+ context_folder=str(context_dir),
112
+ events_log_path=str(context_dir / "events.jsonl"),
113
+ active_tasks=[_task_to_dict(t) for t in pending_tasks],
114
+ completed_tasks_this_session=[
115
+ {"subject": s} for s in (completed_this_session or [])
116
+ ],
117
+ work_summary=work_summary,
118
+ next_steps=next_steps or [],
119
+ important_notes=important_notes or [],
120
+ )
121
+
122
+ # Compute file path BEFORE rendering markdown
123
+ handoffs_dir = get_context_handoffs_dir(context_id, project_root)
124
+ handoffs_dir.mkdir(parents=True, exist_ok=True)
125
+
126
+ # Filename: YYYY-MM-DD-session-{session_id}.md
127
+ date_str = datetime.now().strftime("%Y-%m-%d")
128
+ filename = f"{date_str}-session-{session_id}.md"
129
+ file_path = handoffs_dir / filename
130
+
131
+ # Set file_path on doc BEFORE rendering markdown
132
+ doc.file_path = str(file_path)
133
+
134
+ # Generate markdown content
135
+ markdown = _render_handoff_markdown(doc)
136
+
137
+ # Save to handoffs folder
138
+
139
+ success, error = atomic_write(file_path, markdown)
140
+ if not success:
141
+ eprint(f"[handoff] ERROR: Failed to write handoff document: {error}")
142
+ return None
143
+
144
+ # Record event
145
+ append_event(
146
+ context_id,
147
+ EVENT_HANDOFF_CREATED,
148
+ project_root,
149
+ path=str(file_path),
150
+ reason=reason,
151
+ session_id=session_id
152
+ )
153
+
154
+ # Update context in_flight state
155
+ update_handoff_status(context_id, str(file_path), project_root)
156
+
157
+ eprint(f"[handoff] Created handoff document: {file_path}")
158
+ return doc
159
+
160
+
161
+ def _task_to_dict(task: Task) -> Dict[str, Any]:
162
+ """Convert Task to dictionary for handoff document."""
163
+ return {
164
+ "id": task.id,
165
+ "subject": task.subject,
166
+ "status": task.status,
167
+ "description": task.description,
168
+ }
169
+
170
+
171
+ def _render_handoff_markdown(doc: HandoffDocument) -> str:
172
+ """Render handoff document as markdown."""
173
+ lines = [
174
+ format_continuation_header("handoff", doc.context_id),
175
+ "",
176
+ f"**Created**: {doc.created_at}",
177
+ f"**Context ID**: {doc.context_id}",
178
+ f"**Session ID**: {doc.session_id}",
179
+ f"**Reason**: {format_reason(doc.reason)}",
180
+ "",
181
+ "## Links",
182
+ "",
183
+ ]
184
+
185
+ # Plan link
186
+ if doc.plan_path:
187
+ lines.append(f"- **Plan**: [{Path(doc.plan_path).name}]({doc.plan_path})")
188
+
189
+ lines.extend([
190
+ f"- **Context Folder**: `{doc.context_folder}`",
191
+ f"- **Events Log**: `{doc.events_log_path}`",
192
+ "",
193
+ "## Current State",
194
+ "",
195
+ ])
196
+
197
+ # Active tasks
198
+ lines.append(render_task_list(doc.active_tasks, header="Active Tasks", show_description=True).rstrip())
199
+ lines.append("")
200
+
201
+ # Completed this session
202
+ if doc.completed_tasks_this_session:
203
+ lines.append(render_task_list(
204
+ doc.completed_tasks_this_session,
205
+ header="Completed This Session",
206
+ show_description=False
207
+ ).rstrip())
208
+ lines.append("")
209
+
210
+ # Work summary
211
+ if doc.work_summary:
212
+ lines.extend([
213
+ "## Context Summary",
214
+ "",
215
+ doc.work_summary,
216
+ "",
217
+ ])
218
+
219
+ # Next steps
220
+ if doc.next_steps:
221
+ lines.extend([
222
+ "## Next Steps",
223
+ "",
224
+ ])
225
+ for i, step in enumerate(doc.next_steps, 1):
226
+ lines.append(f"{i}. {step}")
227
+ lines.append("")
228
+
229
+ # Important notes
230
+ if doc.important_notes:
231
+ lines.extend([
232
+ "## Important Notes",
233
+ "",
234
+ ])
235
+ for note in doc.important_notes:
236
+ lines.append(f"- {note}")
237
+ lines.append("")
238
+
239
+ # Continuation prompt
240
+ lines.extend([
241
+ "---",
242
+ "",
243
+ "**Continuation Prompt**:",
244
+ "```",
245
+ f'Continue working on context "{doc.context_id}".',
246
+ "",
247
+ f"Handoff document: {doc.file_path or 'See above'}",
248
+ "",
249
+ "Read the handoff document, restore tasks with TaskCreate, and continue implementation.",
250
+ "```",
251
+ ])
252
+
253
+ return "\n".join(lines)
254
+
255
+
256
+ def get_handoff_continuation_prompt(doc: HandoffDocument) -> str:
257
+ """
258
+ Generate the prompt to paste into new session for continuation.
259
+
260
+ Args:
261
+ doc: HandoffDocument with file_path set
262
+
263
+ Returns:
264
+ Prompt string for continuing work
265
+ """
266
+ return f"""Continue working on context "{doc.context_id}".
267
+
268
+ Handoff document: {doc.file_path}
269
+
270
+ Read the handoff document, restore tasks with TaskCreate, and continue implementation."""
271
+
272
+
273
+ def get_low_context_warning(context_remaining_percent: int, context_id: str) -> str:
274
+ """
275
+ Generate system reminder for low context warning.
276
+
277
+ This is injected by the UserPromptSubmit hook when context is low.
278
+
279
+ Args:
280
+ context_remaining_percent: Percentage of context remaining
281
+ context_id: Current context identifier
282
+
283
+ Returns:
284
+ System reminder markdown
285
+ """
286
+ return f"""<system-reminder>
287
+ ## LOW CONTEXT WARNING ({context_remaining_percent}% remaining)
288
+
289
+ Your context window is running low. Please:
290
+
291
+ 1. **Finish current task** if 1-2 steps away, OR save current progress
292
+ 2. **Create handoff document** by calling:
293
+ ```python
294
+ from _shared.lib.handoff import generate_handoff_document
295
+ doc = generate_handoff_document(
296
+ context_id="{context_id}",
297
+ reason="low_context",
298
+ work_summary="<describe current work>",
299
+ next_steps=["<step 1>", "<step 2>"],
300
+ important_notes=["<key decision 1>"]
301
+ )
302
+ ```
303
+ 3. **Ask permission** to clear and paste continuation prompt
304
+
305
+ After creating handoff, ask the user:
306
+ "Context is low. I've created a handoff document. May I clear and continue in a new session?"
307
+ </system-reminder>"""