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,554 +0,0 @@
1
- """SessionStart discovery utilities for context management.
2
-
3
- Provides functions for discovering contexts at session start and
4
- formatting output for Claude to display context choices.
5
-
6
- Used by:
7
- - SessionStart hook to show available contexts
8
- - Plan handoff flow to auto-continue implementation
9
- """
10
- from pathlib import Path
11
- from typing import List, Optional, Tuple
12
-
13
- from datetime import datetime
14
-
15
- from .context_manager import (
16
- Context,
17
- get_all_contexts,
18
- get_context_with_pending_plan,
19
- get_context_with_in_flight_work,
20
- )
21
- from .event_log import get_current_state, get_pending_tasks, Task
22
- from .auto_state import load_auto_state
23
- from .task_sync import generate_task_summary
24
- from ..base.utils import eprint, parse_iso_timestamp
25
- from ..base.constants import get_context_dir
26
- from ..templates.formatters import get_status_icon, format_continuation_header, get_mode_display
27
-
28
-
29
- def find_plan_path(context: Context, project_root: Path = None) -> Optional[str]:
30
- """
31
- Find the most relevant plan path for a context.
32
-
33
- Priority:
34
- 1. Active plan (in_flight.artifact_path) if file exists
35
- 2. Most recent archived plan by mtime
36
- 3. None if no plans found
37
-
38
- Args:
39
- context: Context to find plan for
40
- project_root: Project root directory
41
-
42
- Returns:
43
- Plan file path string or None
44
- """
45
- # 1. Active plan (in_flight.artifact_path)
46
- if context.in_flight and context.in_flight.artifact_path:
47
- plan_path = Path(context.in_flight.artifact_path)
48
- if plan_path.exists():
49
- return str(plan_path)
50
-
51
- # 2. Archived plans (most recent by mtime)
52
- plans_dir = get_context_dir(context.id, project_root) / "plans"
53
- if plans_dir.exists():
54
- plans = sorted(plans_dir.glob("*.md"), key=lambda p: p.stat().st_mtime, reverse=True)
55
- if plans:
56
- return str(plans[0])
57
-
58
- # 3. No plan found
59
- return None
60
-
61
-
62
- def _build_restore_sections(
63
- context: Context,
64
- project_root: Path = None
65
- ) -> str:
66
- """
67
- Build restoration context sections from auto-state and task history.
68
-
69
- Used by formatters to inject richer context when resuming work.
70
- Returns empty string if no restoration data is available (fresh context).
71
-
72
- Args:
73
- context: Context being restored
74
- project_root: Project root directory
75
-
76
- Returns:
77
- Formatted markdown sections (may be empty)
78
- """
79
- sections = []
80
-
81
- # Load auto-state for git info and session end metadata
82
- auto_state = load_auto_state(context.id, project_root)
83
-
84
- # Add session end info if available
85
- if auto_state:
86
- saved_at = auto_state.get("saved_at", "")
87
- save_reason = auto_state.get("save_reason", "")
88
- if saved_at:
89
- time_str = format_relative_time(saved_at)
90
- reason_display = save_reason.replace("_", " ") if save_reason else "unknown"
91
- sections.append(f"**Last session ended:** {time_str} ({reason_display})")
92
-
93
- # Task summary (session-aware)
94
- task_summary = generate_task_summary(context.id, project_root)
95
- if task_summary and task_summary != "No tasks in this context.":
96
- sections.append("")
97
- sections.append(task_summary)
98
-
99
- # Plan path
100
- plan_path = find_plan_path(context, project_root)
101
- if plan_path:
102
- sections.append("")
103
- sections.append("### Plan")
104
- sections.append(f"Read the plan at: `{plan_path}`")
105
-
106
- # Git state from auto-state
107
- if auto_state:
108
- git_state = auto_state.get("git_state", {})
109
- if git_state:
110
- branch = git_state.get("branch", "unknown")
111
- uncommitted = git_state.get("uncommitted_files", [])
112
- last_commit = git_state.get("last_commit_short", "")
113
-
114
- sections.append("")
115
- sections.append("### Git State")
116
- uncommitted_str = ", ".join(uncommitted[:5]) if uncommitted else "none"
117
- if len(uncommitted) > 5:
118
- uncommitted_str += f" (+{len(uncommitted) - 5} more)"
119
- sections.append(f"Branch: {branch} | Uncommitted: {uncommitted_str}")
120
- if last_commit:
121
- sections.append(f"Last commit: {last_commit}")
122
-
123
- return "\n".join(sections)
124
-
125
-
126
- def discover_contexts_for_session(
127
- project_root: Path = None
128
- ) -> Tuple[List[Context], Optional[Context]]:
129
- """
130
- SessionStart discovery.
131
-
132
- Returns:
133
- Tuple of:
134
- - List of active contexts sorted by last_active (recent first)
135
- - Context with pending plan implementation (if any)
136
- """
137
- active_contexts = get_all_contexts(status="active", project_root=project_root)
138
- pending_plan_context = get_context_with_pending_plan(project_root)
139
-
140
- return active_contexts, pending_plan_context
141
-
142
-
143
- def get_in_flight_context(project_root: Path = None) -> Optional[Context]:
144
- """
145
- Get context with any in-flight work (plan, etc.).
146
-
147
- Priority order:
148
- 1. pending_implementation - plan ready for implementation
149
- 2. implementing - implementation in progress
150
- 3. planning - actively planning
151
-
152
- Args:
153
- project_root: Project root directory
154
-
155
- Returns:
156
- Context with in-flight work, or None
157
- """
158
- contexts = get_all_contexts(status="active", project_root=project_root)
159
-
160
- # Sort by in-flight priority
161
- priority_order = {
162
- "pending_implementation": 0,
163
- "implementing": 1,
164
- "planning": 2,
165
- "none": 99,
166
- }
167
-
168
- # Only auto-continue for high-priority modes (not "implementing", "planning" or "none")
169
- actionable_modes = {"pending_implementation"}
170
-
171
- in_flight_contexts = [
172
- c for c in contexts
173
- if c.in_flight and c.in_flight.mode in actionable_modes
174
- ]
175
-
176
- if not in_flight_contexts:
177
- return None
178
-
179
- # Return highest priority, with secondary sort by last_active (most recent) for determinism
180
- in_flight_contexts.sort(
181
- key=lambda c: (
182
- priority_order.get(c.in_flight.mode, 99),
183
- -(parse_iso_timestamp(c.last_active) or datetime.min).timestamp() if c.last_active else 0
184
- )
185
- )
186
- return in_flight_contexts[0]
187
-
188
-
189
- def format_context_list(contexts: List[Context]) -> str:
190
- """
191
- Format contexts for display to user in SessionStart.
192
-
193
- Shows context name, summary, status, and last activity time.
194
-
195
- Args:
196
- contexts: List of contexts to format
197
-
198
- Returns:
199
- Formatted markdown string for display
200
- """
201
- if not contexts:
202
- return "No active contexts found."
203
-
204
- lines = ["## Active Contexts\n"]
205
-
206
- for i, ctx in enumerate(contexts, 1):
207
- # Format last active time
208
- time_str = format_relative_time(ctx.last_active)
209
-
210
- # Build status indicator
211
- status_indicator = ""
212
- if ctx.in_flight and ctx.in_flight.mode != "none":
213
- mode_display = get_mode_display(ctx.in_flight.mode)
214
- if mode_display:
215
- status_indicator = f" {mode_display}"
216
-
217
- lines.append(f"**{i}. {ctx.id}**{status_indicator}")
218
- lines.append(f" {ctx.summary}")
219
- if ctx.method:
220
- lines.append(f" Method: {ctx.method} | Last active: {time_str}")
221
- else:
222
- lines.append(f" Last active: {time_str}")
223
- lines.append("")
224
-
225
- return "\n".join(lines)
226
-
227
-
228
- def format_pending_plan_continuation(context: Context, project_root: Path = None) -> str:
229
- """
230
- Format output for plan handoff scenario.
231
-
232
- This is shown when SessionStart detects a context with
233
- plan.status = "pending_implementation". Provides Claude
234
- with instructions to continue implementation.
235
-
236
- Args:
237
- context: Context with pending plan implementation
238
- project_root: Project root directory
239
-
240
- Returns:
241
- Formatted instructions for Claude
242
- """
243
- lines = [
244
- f"## Resuming Context: {context.id}",
245
- "",
246
- f"**Summary:** {context.summary}",
247
- f"**Mode:** Pending Implementation",
248
- ]
249
-
250
- # Add restore sections (auto-state, tasks, git)
251
- restore = _build_restore_sections(context, project_root)
252
- if restore:
253
- lines.append(restore)
254
-
255
- lines.extend([
256
- "",
257
- "---",
258
- "",
259
- "**Instructions:**",
260
- "1. Review the plan and previous work above",
261
- "2. Continue from where the previous session left off",
262
- ])
263
-
264
- return "\n".join(lines)
265
-
266
-
267
- def format_implementation_continuation(context: Context, project_root: Path = None) -> str:
268
- """
269
- Format output for ongoing implementation scenario.
270
-
271
- This is shown when SessionStart detects a context with
272
- in_flight.mode = "implementing".
273
-
274
- Args:
275
- context: Context with implementation in progress
276
- project_root: Project root directory
277
-
278
- Returns:
279
- Formatted instructions for Claude
280
- """
281
- lines = [
282
- f"## Resuming Context: {context.id}",
283
- "",
284
- f"**Summary:** {context.summary}",
285
- f"**Mode:** Implementing",
286
- ]
287
-
288
- # Add restore sections (auto-state, tasks, git)
289
- restore = _build_restore_sections(context, project_root)
290
- if restore:
291
- lines.append(restore)
292
-
293
- lines.extend([
294
- "",
295
- "---",
296
- "",
297
- "**Instructions:**",
298
- "1. Review the plan and previous work above",
299
- "2. Continue from where the previous session left off",
300
- ])
301
-
302
- return "\n".join(lines)
303
-
304
-
305
- def format_context_picker_prompt() -> str:
306
- """
307
- Format the prompt asking user which context to continue.
308
-
309
- Returns:
310
- Prompt string for user
311
- """
312
- return (
313
- "\nWhich context would you like to continue?\n"
314
- "(Say the name/number, or 'new' to start fresh)"
315
- )
316
-
317
-
318
- def format_ready_for_new_work() -> str:
319
- """
320
- Format output when no active contexts exist.
321
-
322
- Returns:
323
- Ready message for user
324
- """
325
- return "No active contexts. Ready for new work."
326
-
327
-
328
- def parse_context_choice_from_prompt(prompt: str, contexts: List[Context]) -> Optional[str]:
329
- """
330
- Parse context selection from user prompt.
331
-
332
- Looks for patterns like:
333
- - "continue feature-auth" or "resume feature-auth"
334
- - "1" or "2" (number selection)
335
- - Context ID mentioned in prompt
336
-
337
- Args:
338
- prompt: User's prompt text
339
- contexts: Available contexts to match against
340
-
341
- Returns:
342
- Context ID if match found, None otherwise
343
- """
344
- if not prompt or not contexts:
345
- return None
346
-
347
- prompt_lower = prompt.lower().strip()
348
-
349
- # Check for number selection (1, 2, 3, etc.)
350
- # Match single digit at start or "option 1", "number 1", etc.
351
- import re
352
- number_match = re.match(r'^(\d+)$', prompt_lower)
353
- if number_match:
354
- idx = int(number_match.group(1)) - 1 # 1-indexed
355
- if 0 <= idx < len(contexts):
356
- return contexts[idx].id
357
-
358
- # Check for "continue X" or "resume X" patterns
359
- continue_match = re.match(r'^(?:continue|resume|work on|back to)\s+(.+)$', prompt_lower)
360
- if continue_match:
361
- target = continue_match.group(1).strip()
362
- # Try to match against context IDs
363
- for ctx in contexts:
364
- if ctx.id.lower() == target or target in ctx.id.lower():
365
- return ctx.id
366
-
367
- # Check if any context ID appears in the prompt
368
- for ctx in contexts:
369
- if ctx.id.lower() in prompt_lower:
370
- return ctx.id
371
-
372
- return None
373
-
374
-
375
- def format_context_selection_required(contexts: List[Context]) -> str:
376
- """
377
- Format urgent picker prompt when multiple contexts require selection.
378
-
379
- Used by context enforcer hook when context cannot be auto-determined.
380
-
381
- Args:
382
- contexts: Available contexts to choose from
383
-
384
- Returns:
385
- Formatted system reminder with context choices
386
- """
387
- lines = [
388
- "## Context Selection Required",
389
- "",
390
- "Multiple active contexts exist. Please indicate which to continue:",
391
- "",
392
- ]
393
-
394
- for i, ctx in enumerate(contexts, 1):
395
- time_str = format_relative_time(ctx.last_active)
396
-
397
- # Add status indicator for in-flight work
398
- status = ""
399
- if ctx.in_flight and ctx.in_flight.mode != "none":
400
- mode_display = get_mode_display(ctx.in_flight.mode)
401
- if mode_display:
402
- status = f" {mode_display}"
403
-
404
- lines.append(f"{i}. **{ctx.id}**{status} - {ctx.summary} [{time_str}]")
405
-
406
- lines.extend([
407
- "",
408
- "Say the number/name, or describe your new work (a context will be created).",
409
- ])
410
-
411
- return "\n".join(lines)
412
-
413
-
414
- def format_active_context_reminder(
415
- context: Context,
416
- project_root: Path = None,
417
- include_restore: bool = False
418
- ) -> str:
419
- """
420
- Format system reminder for active context.
421
-
422
- Called in two situations:
423
- 1. session_match (every prompt): include_restore=False → lightweight
424
- 2. auto_selected first bind: include_restore=True → rich restore context
425
-
426
- Args:
427
- context: Active context
428
- project_root: Project root directory
429
- include_restore: If True, include auto-state/tasks/git restore sections.
430
- Only set True on first bind to avoid per-prompt overhead.
431
-
432
- Returns:
433
- Formatted system reminder
434
- """
435
- time_str = format_relative_time(context.last_active)
436
-
437
- # Build mode display
438
- mode_display = "Active"
439
- if context.in_flight and context.in_flight.mode != "none":
440
- # Get mode display and strip brackets for this usage
441
- mode_str = get_mode_display(context.in_flight.mode)
442
- if mode_str:
443
- # Remove brackets from "[Planning]" to get "Planning"
444
- mode_display = mode_str.strip("[]")
445
-
446
- if include_restore:
447
- # Rich restore: first bind to existing context in new session
448
- lines = [
449
- f"## Resuming Context: {context.id}",
450
- "",
451
- f"**Summary:** {context.summary}",
452
- f"**Mode:** {mode_display}",
453
- ]
454
-
455
- restore = _build_restore_sections(context, project_root)
456
- if restore:
457
- lines.append(restore)
458
-
459
- lines.extend([
460
- "",
461
- "---",
462
- "",
463
- "**Instructions:**",
464
- "1. Review the previous work above",
465
- "2. Continue from where the previous session left off",
466
- ])
467
- else:
468
- # Lightweight: subsequent prompts in same session
469
- lines = [
470
- f"## Active Context: {context.id}",
471
- "",
472
- f"**Summary:** {context.summary}",
473
- f"**Mode:** {mode_display}",
474
- f"**Last Active:** {time_str}",
475
- "",
476
- f'All work belongs to context "{context.id}".',
477
- "Tasks created with TaskCreate will be persisted to this context.",
478
- ]
479
-
480
- return "\n".join(lines)
481
-
482
-
483
- def format_context_created(context: Context) -> str:
484
- """
485
- Format notification that a new context was auto-created.
486
-
487
- Args:
488
- context: Newly created context
489
-
490
- Returns:
491
- Formatted system reminder
492
- """
493
- lines = [
494
- f"## Context Created: {context.id}",
495
- "",
496
- f"**Summary:** {context.summary}",
497
- "",
498
- "A new context has been created for this work.",
499
- "Tasks created with TaskCreate will be persisted to this context.",
500
- ]
501
-
502
- return "\n".join(lines)
503
-
504
-
505
- def format_relative_time(iso_timestamp: Optional[str]) -> str:
506
- """
507
- Format ISO timestamp as relative time string.
508
-
509
- Args:
510
- iso_timestamp: ISO format timestamp string
511
-
512
- Returns:
513
- Relative time string like "2 hours ago" or "yesterday"
514
- """
515
- if not iso_timestamp:
516
- return "unknown"
517
-
518
- dt = parse_iso_timestamp(iso_timestamp)
519
- if not dt:
520
- return iso_timestamp[:16] # Fallback: show date/time portion
521
-
522
- now = datetime.now()
523
-
524
- # Handle timezone-aware vs naive datetime comparison
525
- # If dt is timezone-aware, convert to naive for comparison
526
- if dt.tzinfo is not None:
527
- try:
528
- # Convert to local time and strip timezone
529
- dt = dt.replace(tzinfo=None)
530
- except Exception:
531
- return iso_timestamp[:16] # Fallback on error
532
-
533
- diff = now - dt
534
-
535
- if diff.days == 0:
536
- hours = diff.seconds // 3600
537
- if hours == 0:
538
- minutes = diff.seconds // 60
539
- if minutes == 0:
540
- return "just now"
541
- elif minutes == 1:
542
- return "1 minute ago"
543
- else:
544
- return f"{minutes} minutes ago"
545
- elif hours == 1:
546
- return "1 hour ago"
547
- else:
548
- return f"{hours} hours ago"
549
- elif diff.days == 1:
550
- return "yesterday"
551
- elif diff.days < 7:
552
- return f"{diff.days} days ago"
553
- else:
554
- return dt.strftime("%Y-%m-%d")