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,308 @@
1
+ """Event log utilities for context management.
2
+
3
+ events.jsonl is the SOURCE OF TRUTH for each context.
4
+ All state is derived by replaying these events.
5
+
6
+ Event format (one JSON object per line):
7
+ {"event": "event_type", "timestamp": "ISO8601", ...event-specific fields}
8
+
9
+ Crash safety:
10
+ - Append-only file
11
+ - Each line is independent JSON
12
+ - Corrupted lines are skipped with warning
13
+ - Valid events before corruption are preserved
14
+ """
15
+ import json
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ from ..base.atomic_write import atomic_append
22
+ from ..base.constants import get_events_file_path
23
+ from ..base.utils import eprint, now_iso
24
+
25
+
26
+ # Event type constants
27
+ EVENT_CONTEXT_CREATED = "context_created"
28
+ EVENT_CONTEXT_COMPLETED = "context_completed"
29
+ EVENT_CONTEXT_REOPENED = "context_reopened"
30
+ EVENT_CONTEXT_ARCHIVED = "context_archived"
31
+ EVENT_METADATA_UPDATED = "metadata_updated"
32
+ EVENT_TASK_ADDED = "task_added"
33
+ EVENT_TASK_STARTED = "task_started"
34
+ EVENT_TASK_COMPLETED = "task_completed"
35
+ EVENT_TASK_BLOCKED = "task_blocked"
36
+ EVENT_NOTE_ADDED = "note_added"
37
+ EVENT_SESSION_STARTED = "session_started"
38
+ EVENT_PLANNING_STARTED = "planning_started"
39
+ EVENT_PLAN_CREATED = "plan_created"
40
+ EVENT_PLAN_IMPLEMENTATION_STARTED = "plan_implementation_started"
41
+ EVENT_PLAN_COMPLETED = "plan_completed"
42
+ EVENT_HANDOFF_CREATED = "handoff_created"
43
+ EVENT_HANDOFF_CLEARED = "handoff_cleared"
44
+
45
+
46
+ @dataclass
47
+ class Task:
48
+ """Task state derived from events."""
49
+ id: str
50
+ subject: str
51
+ description: str = ""
52
+ active_form: str = ""
53
+ status: str = "pending" # pending, in_progress, completed, blocked
54
+ evidence: str = ""
55
+ work_summary: str = ""
56
+ files_changed: List[str] = field(default_factory=list)
57
+ blocked_reason: str = ""
58
+
59
+
60
+ @dataclass
61
+ class ContextState:
62
+ """Current state of a context, derived from events."""
63
+ id: str
64
+ status: str = "active" # active, completed
65
+ summary: str = ""
66
+ method: Optional[str] = None
67
+ tags: List[str] = field(default_factory=list)
68
+ created_at: Optional[str] = None
69
+ last_active: Optional[str] = None
70
+ tasks: List[Task] = field(default_factory=list)
71
+ notes: List[str] = field(default_factory=list)
72
+ plan_status: str = "none" # none, planning, pending_implementation, implementing
73
+ plan_path: Optional[str] = None
74
+ plan_hash: Optional[str] = None
75
+
76
+
77
+ def read_events(context_id: str, project_root: Path = None) -> List[Dict[str, Any]]:
78
+ """
79
+ Read all events from a context's events.jsonl file.
80
+
81
+ Handles corrupted lines gracefully by skipping them with a warning.
82
+
83
+ Args:
84
+ context_id: Context identifier
85
+ project_root: Project root directory (default: cwd)
86
+
87
+ Returns:
88
+ List of event dictionaries, in chronological order
89
+ """
90
+ events_path = get_events_file_path(context_id, project_root)
91
+
92
+ if not events_path.exists():
93
+ return []
94
+
95
+ events = []
96
+ try:
97
+ content = events_path.read_text(encoding='utf-8')
98
+ for line_num, line in enumerate(content.splitlines(), 1):
99
+ line = line.strip()
100
+ if not line:
101
+ continue
102
+
103
+ try:
104
+ event = json.loads(line)
105
+ events.append(event)
106
+ except json.JSONDecodeError:
107
+ eprint(f"[event_log] WARNING: Skipping corrupted line {line_num} in {events_path}")
108
+
109
+ except UnicodeDecodeError as e:
110
+ eprint(f"[event_log] WARNING: Invalid UTF-8 in events file {events_path}, attempting fallback read")
111
+ # Try reading with error handling to salvage what we can
112
+ try:
113
+ content = events_path.read_text(encoding='utf-8', errors='replace')
114
+ for line_num, line in enumerate(content.splitlines(), 1):
115
+ line = line.strip()
116
+ if not line:
117
+ continue
118
+
119
+ try:
120
+ event = json.loads(line)
121
+ events.append(event)
122
+ except json.JSONDecodeError:
123
+ eprint(f"[event_log] WARNING: Skipping corrupted line {line_num} in {events_path}")
124
+ except Exception as fallback_error:
125
+ eprint(f"[event_log] ERROR: Fallback read failed: {fallback_error}")
126
+
127
+ except Exception as e:
128
+ eprint(f"[event_log] ERROR reading events file: {e}")
129
+
130
+ return events
131
+
132
+
133
+ def append_event(
134
+ context_id: str,
135
+ event_type: str,
136
+ project_root: Path = None,
137
+ **event_data
138
+ ) -> bool:
139
+ """
140
+ Append an event to a context's events.jsonl file.
141
+
142
+ Args:
143
+ context_id: Context identifier
144
+ event_type: Type of event (e.g., "task_added", "context_completed")
145
+ project_root: Project root directory (default: cwd)
146
+ **event_data: Additional event-specific data
147
+
148
+ Returns:
149
+ True if event was successfully appended
150
+ """
151
+ events_path = get_events_file_path(context_id, project_root)
152
+
153
+ event = {
154
+ "event": event_type,
155
+ "timestamp": now_iso(),
156
+ **event_data
157
+ }
158
+
159
+ try:
160
+ event_json = json.dumps(event, ensure_ascii=False)
161
+ success, error = atomic_append(events_path, event_json + "\n")
162
+
163
+ if not success:
164
+ eprint(f"[event_log] ERROR appending event: {error}")
165
+ return False
166
+
167
+ return True
168
+
169
+ except Exception as e:
170
+ eprint(f"[event_log] ERROR serializing event: {e}")
171
+ return False
172
+
173
+
174
+ def get_current_state(context_id: str, project_root: Path = None) -> ContextState:
175
+ """
176
+ Compute current context state by replaying events.
177
+
178
+ This is the canonical way to determine current state -
179
+ everything is derived from events.jsonl.
180
+
181
+ Args:
182
+ context_id: Context identifier
183
+ project_root: Project root directory (default: cwd)
184
+
185
+ Returns:
186
+ ContextState representing current state
187
+ """
188
+ events = read_events(context_id, project_root)
189
+
190
+ state = ContextState(id=context_id)
191
+ tasks_map: Dict[str, Task] = {}
192
+
193
+ for event in events:
194
+ event_type = event.get("event")
195
+ timestamp = event.get("timestamp")
196
+
197
+ # Update last_active for any event
198
+ state.last_active = timestamp
199
+
200
+ if event_type == EVENT_CONTEXT_CREATED:
201
+ state.summary = event.get("summary", "")
202
+ state.method = event.get("method")
203
+ state.tags = event.get("tags", [])
204
+ state.created_at = timestamp
205
+
206
+ elif event_type == EVENT_CONTEXT_COMPLETED:
207
+ state.status = "completed"
208
+
209
+ elif event_type == EVENT_CONTEXT_REOPENED:
210
+ state.status = "active"
211
+
212
+ elif event_type == EVENT_METADATA_UPDATED:
213
+ if "summary" in event:
214
+ state.summary = event["summary"]
215
+ if "tags" in event:
216
+ state.tags = event["tags"]
217
+ if "method" in event:
218
+ state.method = event["method"]
219
+
220
+ elif event_type == EVENT_TASK_ADDED:
221
+ task_id = event.get("task_id")
222
+ if task_id:
223
+ tasks_map[task_id] = Task(
224
+ id=task_id,
225
+ subject=event.get("subject", ""),
226
+ description=event.get("description", ""),
227
+ active_form=event.get("activeForm", ""),
228
+ status="pending"
229
+ )
230
+
231
+ elif event_type == EVENT_TASK_STARTED:
232
+ task_id = event.get("task_id")
233
+ if task_id and task_id in tasks_map:
234
+ tasks_map[task_id].status = "in_progress"
235
+
236
+ elif event_type == EVENT_TASK_COMPLETED:
237
+ task_id = event.get("task_id")
238
+ if task_id and task_id in tasks_map:
239
+ task = tasks_map[task_id]
240
+ task.status = "completed"
241
+ task.evidence = event.get("evidence", "")
242
+ task.work_summary = event.get("work_summary", "")
243
+ task.files_changed = event.get("files_changed", [])
244
+
245
+ elif event_type == EVENT_TASK_BLOCKED:
246
+ task_id = event.get("task_id")
247
+ if task_id and task_id in tasks_map:
248
+ tasks_map[task_id].status = "blocked"
249
+ tasks_map[task_id].blocked_reason = event.get("reason", "")
250
+
251
+ elif event_type == EVENT_NOTE_ADDED:
252
+ note = event.get("content", "")
253
+ if note:
254
+ state.notes.append(note)
255
+
256
+ elif event_type == EVENT_PLAN_CREATED:
257
+ state.plan_status = "pending_implementation"
258
+ state.plan_path = event.get("path")
259
+ state.plan_hash = event.get("hash")
260
+
261
+ elif event_type == EVENT_PLAN_IMPLEMENTATION_STARTED:
262
+ state.plan_status = "implementing"
263
+
264
+ elif event_type == EVENT_PLAN_COMPLETED:
265
+ state.plan_status = "none"
266
+ state.plan_path = None
267
+ state.plan_hash = None
268
+
269
+ # Convert tasks map to list
270
+ state.tasks = list(tasks_map.values())
271
+
272
+ return state
273
+
274
+
275
+ def are_all_tasks_completed(context_id: str, project_root: Path = None) -> bool:
276
+ """
277
+ Check if all tasks in a context are completed.
278
+
279
+ Useful for suggesting context completion to user.
280
+
281
+ Args:
282
+ context_id: Context identifier
283
+ project_root: Project root directory (default: cwd)
284
+
285
+ Returns:
286
+ True if all tasks are completed (or no tasks exist)
287
+ """
288
+ state = get_current_state(context_id, project_root)
289
+
290
+ if not state.tasks:
291
+ return True # No tasks = trivially complete
292
+
293
+ return all(task.status == "completed" for task in state.tasks)
294
+
295
+
296
+ def get_pending_tasks(context_id: str, project_root: Path = None) -> List[Task]:
297
+ """
298
+ Get all non-completed tasks from a context.
299
+
300
+ Args:
301
+ context_id: Context identifier
302
+ project_root: Project root directory (default: cwd)
303
+
304
+ Returns:
305
+ List of tasks that are not completed
306
+ """
307
+ state = get_current_state(context_id, project_root)
308
+ return [t for t in state.tasks if t.status != "completed"]
@@ -0,0 +1,247 @@
1
+ """Plan archive utilities for context management.
2
+
3
+ Provides functions for archiving plans to context folders and
4
+ managing plan lifecycle.
5
+
6
+ Used by:
7
+ - ExitPlanMode hook to archive approved plans
8
+ - SessionStart to detect pending implementations
9
+ """
10
+ import hashlib
11
+ import shutil
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Optional, Tuple
15
+
16
+ from .context_manager import (
17
+ Context,
18
+ create_context,
19
+ get_context,
20
+ get_all_contexts,
21
+ update_plan_status,
22
+ )
23
+ from .event_log import append_event, EVENT_PLAN_CREATED
24
+ from ..base.atomic_write import atomic_write
25
+ from ..base.constants import get_context_plans_dir
26
+ from ..base.utils import eprint, now_iso, sanitize_title
27
+
28
+
29
+ def archive_plan_to_context(
30
+ plan_path: str,
31
+ context_id: str,
32
+ project_root: Path = None
33
+ ) -> Tuple[Optional[str], Optional[str]]:
34
+ """
35
+ Archive plan to context's plans folder.
36
+
37
+ Actions:
38
+ 1. Copy plan to _output/contexts/<context_id>/plans/<date>-<slug>.md
39
+ 2. Compute plan hash for change detection
40
+ 3. Update context.json: in_flight.mode = "pending_implementation"
41
+ 4. Update context.json: in_flight.artifact_path = archived path
42
+
43
+ Args:
44
+ plan_path: Path to the plan file to archive
45
+ context_id: Target context ID
46
+ project_root: Project root directory
47
+
48
+ Returns:
49
+ Tuple of (archived_path, plan_hash) or (None, None) on error
50
+ """
51
+ plan_file = Path(plan_path)
52
+ if not plan_file.exists():
53
+ eprint(f"[plan_archive] Plan file not found: {plan_path}")
54
+ return None, None
55
+
56
+ # Read plan content
57
+ try:
58
+ plan_content = plan_file.read_text(encoding='utf-8')
59
+ except Exception as e:
60
+ eprint(f"[plan_archive] Failed to read plan: {e}")
61
+ return None, None
62
+
63
+ # Compute hash for change detection
64
+ plan_hash = hashlib.sha256(plan_content.encode('utf-8')).hexdigest()[:12]
65
+
66
+ # Create plans directory
67
+ plans_dir = get_context_plans_dir(context_id, project_root)
68
+ plans_dir.mkdir(parents=True, exist_ok=True)
69
+
70
+ # Generate archive filename: YYYY-MM-DD-<slug>.md
71
+ date_str = datetime.now().strftime("%Y-%m-%d")
72
+ slug = sanitize_title(plan_file.stem, max_len=30)
73
+ archive_name = f"{date_str}-{slug}.md"
74
+ archive_path = plans_dir / archive_name
75
+
76
+ # Handle name collision
77
+ counter = 2
78
+ while archive_path.exists():
79
+ archive_name = f"{date_str}-{slug}-{counter}.md"
80
+ archive_path = plans_dir / archive_name
81
+ counter += 1
82
+
83
+ # Write archived plan
84
+ success, error = atomic_write(archive_path, plan_content)
85
+ if not success:
86
+ eprint(f"[plan_archive] Failed to write archive: {error}")
87
+ return None, None
88
+
89
+ # Update context plan status
90
+ update_plan_status(
91
+ context_id,
92
+ status="pending_implementation",
93
+ path=str(archive_path),
94
+ hash=plan_hash,
95
+ project_root=project_root
96
+ )
97
+
98
+ eprint(f"[plan_archive] Archived plan to: {archive_path}")
99
+ return str(archive_path), plan_hash
100
+
101
+
102
+ def get_active_context_for_plan(
103
+ plan_path: str,
104
+ project_root: Path = None
105
+ ) -> Optional[str]:
106
+ """
107
+ Determine which context a plan belongs to.
108
+
109
+ Logic:
110
+ 1. If exactly one active context exists -> use it
111
+ 2. Check if plan path contains context hints
112
+ 3. Return None if ambiguous
113
+
114
+ Args:
115
+ plan_path: Path to plan file
116
+ project_root: Project root directory
117
+
118
+ Returns:
119
+ Context ID or None if cannot determine
120
+ """
121
+ active_contexts = get_all_contexts(status="active", project_root=project_root)
122
+
123
+ # If exactly one active context, use it
124
+ if len(active_contexts) == 1:
125
+ return active_contexts[0].id
126
+
127
+ # Check if plan path contains a context ID
128
+ plan_path_lower = plan_path.lower()
129
+ for ctx in active_contexts:
130
+ if ctx.id.lower() in plan_path_lower:
131
+ return ctx.id
132
+
133
+ # Check plan filename for context hints
134
+ plan_file = Path(plan_path)
135
+ filename_lower = plan_file.stem.lower()
136
+ for ctx in active_contexts:
137
+ if ctx.id.lower() in filename_lower:
138
+ return ctx.id
139
+
140
+ # If no active contexts, return None (caller should create new context)
141
+ if not active_contexts:
142
+ return None
143
+
144
+ # Multiple active contexts, cannot determine
145
+ eprint(f"[plan_archive] Multiple active contexts, cannot determine target")
146
+ return None
147
+
148
+
149
+ def create_context_from_plan(
150
+ plan_path: str,
151
+ project_root: Path = None
152
+ ) -> Optional[str]:
153
+ """
154
+ Create a new context based on a plan file.
155
+
156
+ Extracts context ID and summary from plan filename/content.
157
+
158
+ Args:
159
+ plan_path: Path to plan file
160
+ project_root: Project root directory
161
+
162
+ Returns:
163
+ Created context ID or None on error
164
+ """
165
+ plan_file = Path(plan_path)
166
+ if not plan_file.exists():
167
+ eprint(f"[plan_archive] Plan file not found: {plan_path}")
168
+ return None
169
+
170
+ # Generate context ID from plan filename
171
+ context_id = sanitize_title(plan_file.stem, max_len=50)
172
+
173
+ # Try to extract summary from plan content
174
+ try:
175
+ content = plan_file.read_text(encoding='utf-8')
176
+ # Look for first heading as summary
177
+ for line in content.splitlines():
178
+ line = line.strip()
179
+ if line.startswith('#'):
180
+ summary = line.lstrip('#').strip()[:100]
181
+ break
182
+ else:
183
+ summary = f"Implementation of {plan_file.stem}"
184
+ except Exception:
185
+ summary = f"Implementation of {plan_file.stem}"
186
+
187
+ # Create the context
188
+ try:
189
+ context = create_context(
190
+ context_id=context_id,
191
+ summary=summary,
192
+ method="cc-native",
193
+ project_root=project_root
194
+ )
195
+ return context.id
196
+ except ValueError as e:
197
+ eprint(f"[plan_archive] Failed to create context: {e}")
198
+ return None
199
+
200
+
201
+ def mark_plan_implementation_started(
202
+ context_id: str,
203
+ project_root: Path = None
204
+ ) -> bool:
205
+ """
206
+ Mark that plan implementation has started.
207
+
208
+ Called by SessionStart after detecting pending_implementation.
209
+ Prevents re-triggering on subsequent /clear commands.
210
+
211
+ Args:
212
+ context_id: Context identifier
213
+ project_root: Project root directory
214
+
215
+ Returns:
216
+ True if successful
217
+ """
218
+ context = update_plan_status(
219
+ context_id,
220
+ status="implementing",
221
+ project_root=project_root
222
+ )
223
+ return context is not None
224
+
225
+
226
+ def mark_plan_completed(
227
+ context_id: str,
228
+ project_root: Path = None
229
+ ) -> bool:
230
+ """
231
+ Mark that plan has been fully implemented.
232
+
233
+ Called when all plan tasks are completed.
234
+
235
+ Args:
236
+ context_id: Context identifier
237
+ project_root: Project root directory
238
+
239
+ Returns:
240
+ True if successful
241
+ """
242
+ context = update_plan_status(
243
+ context_id,
244
+ status="none",
245
+ project_root=project_root
246
+ )
247
+ return context is not None