aiwcli 0.9.8 → 0.10.1

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 (115) 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 +114 -60
  21. package/dist/templates/_shared/hooks/session_start.py +127 -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 +47 -81
  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 +207 -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 +317 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +508 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +653 -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 +22 -37
  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 +31 -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 +37 -14
  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 +54 -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 +76 -89
  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_questions_early.py +81 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  78. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  79. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  86. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  87. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  96. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  97. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  98. package/oclif.manifest.json +1 -1
  99. package/package.json +1 -1
  100. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  101. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  102. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  103. package/dist/templates/_shared/lib/context/cache.py +0 -444
  104. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  105. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  106. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  107. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  108. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  109. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  110. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  111. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  115. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -1,625 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Context enforcer hook - ensures all work happens within a named context.
3
-
4
- This hook runs on UserPromptSubmit to determine the active context.
5
- It enforces that every user interaction happens within a tracked context.
6
-
7
- Context selection priority:
8
- 1. Session already in context -> Continue in that context (highest priority - prevents switching)
9
- 2. Bare "^" -> Show context picker
10
- 3. Explicit caret commands (^E, ^S, ^0, ^N) -> Process as specified
11
- 4. No caret prefix:
12
- - 0 in-flight contexts -> Auto-create new context from prompt
13
- - 1 in-flight context -> Auto-select that context
14
- - Multiple in-flight contexts -> Block and show picker
15
-
16
- In-flight modes: planning, pending_implementation, implementing
17
-
18
- Prefix syntax:
19
- - ^: Show context picker (bare caret)
20
- - ^0 <description>: Create new context (description requires 10+ chars)
21
- - ^1, ^2, etc: Select existing context by number (shorthand for ^S1, ^S2)
22
- - ^E<N>: End/complete context N (removes from active list)
23
- - ^E*: End/complete ALL active contexts
24
- - ^S<N>: Select context N for this session
25
- - Chaining: ^E1E2S3 means end contexts 1 and 2, then select context 3
26
-
27
- Hook input (from Claude Code):
28
- {
29
- "hook_type": "UserPromptSubmit",
30
- "prompt": "user's message text",
31
- "session_id": "abc123",
32
- ...
33
- }
34
-
35
- Hook output:
36
- - Exit 0 + stdout: Context selected, continues with system reminder
37
- - Exit 2 + stderr: Block request, show context picker to user
38
- """
39
- import json
40
- import os
41
- import re
42
- import sys
43
- from dataclasses import dataclass
44
- from pathlib import Path
45
- from typing import List, Optional, Tuple
46
-
47
- # Add parent directories to path for imports
48
- SCRIPT_DIR = Path(__file__).resolve().parent
49
- SHARED_LIB = SCRIPT_DIR.parent / "lib"
50
- sys.path.insert(0, str(SHARED_LIB.parent))
51
-
52
- from lib.base.subprocess_utils import is_internal_call
53
- from lib.context.context_manager import (
54
- Context,
55
- get_all_contexts,
56
- get_all_in_flight_contexts,
57
- create_context_from_prompt,
58
- get_context_by_session_id,
59
- complete_context,
60
- update_plan_status,
61
- )
62
- from lib.context.discovery import (
63
- get_in_flight_context,
64
- format_active_context_reminder,
65
- format_context_created,
66
- format_pending_plan_continuation,
67
- format_implementation_continuation,
68
- format_relative_time,
69
- )
70
- from lib.templates.formatters import get_mode_display
71
- from lib.base.utils import eprint, project_dir
72
-
73
- # Minimum characters required for new context description
74
- MIN_NEW_CONTEXT_CHARS = 10
75
-
76
-
77
- @dataclass
78
- class CaretCommand:
79
- """Parsed caret command result."""
80
- ends: List[int] # Context numbers to end (1-indexed)
81
- select: Optional[int] # Context number to select (1-indexed), None if not specified
82
- new_context_desc: Optional[str] # Description for new context (^0)
83
- remaining_prompt: str # The remaining prompt after the command
84
-
85
-
86
- def parse_chained_caret(prompt: str, contexts: List["Context"]) -> Tuple[Optional[CaretCommand], Optional[str]]:
87
- """
88
- Parse chained caret commands from user prompt.
89
-
90
- Syntax:
91
- - ^E<N>: End context N
92
- - ^E<N>+: End context N and all after (e.g., ^E2+ ends 2, 3, 4, ...)
93
- - ^E*: End ALL contexts
94
- - ^S<N>: Select context N
95
- - ^0 <desc>: Create new context (special case)
96
- - ^<N>: Shorthand for ^S<N> (backwards compat)
97
- - Chain: ^E1E2S3 means end 1, end 2, select 3
98
-
99
- Returns:
100
- Tuple of:
101
- - CaretCommand with parsed actions, or None if no caret prefix
102
- - Error message if syntax is invalid, or None
103
- """
104
- if not prompt.startswith("^"):
105
- return None, None
106
-
107
- # Find where the command ends and the remaining prompt begins
108
- # Command is everything until first whitespace after ^
109
- match = re.match(r'^\^(\S+)(?:\s+(.*))?$', prompt, re.DOTALL)
110
- if not match:
111
- return None, "Invalid prefix. Use ^E<N> to end, ^S<N> to select, or ^0 <desc> for new context."
112
-
113
- command_str = match.group(1)
114
- remaining = (match.group(2) or "").strip()
115
-
116
- # Handle backwards compat: ^N where N is just a number (shorthand for ^SN)
117
- if command_str.isdigit():
118
- num = int(command_str)
119
- if num == 0:
120
- # ^0 <description> - create new context
121
- if len(remaining) < MIN_NEW_CONTEXT_CHARS:
122
- return None, (
123
- f"Please provide a longer description for your new context.\n"
124
- f"Your description '{remaining}' is only {len(remaining)} characters.\n"
125
- f"Minimum required: {MIN_NEW_CONTEXT_CHARS} characters.\n"
126
- f"Example: ^0 implement user authentication with JWT tokens"
127
- )
128
- return CaretCommand(ends=[], select=None, new_context_desc=remaining, remaining_prompt=""), None
129
- else:
130
- # ^N - shorthand for select context N
131
- if num < 1 or num > len(contexts):
132
- if len(contexts) == 0:
133
- return None, "No existing contexts. Use ^0 <description> to create a new one."
134
- return None, f"Invalid selection. Choose 1-{len(contexts)} for existing contexts, or ^0 for new."
135
- # Validate context is in "implementing" mode
136
- ctx = contexts[num - 1]
137
- if not ctx.in_flight or ctx.in_flight.mode != "implementing":
138
- mode = ctx.in_flight.mode if ctx.in_flight else "none"
139
- return None, (
140
- f"Cannot select context {num} ({ctx.id}) - mode is '{mode}'.\n"
141
- f"Only contexts in 'implementing' mode can be selected.\n"
142
- f"Use ^E{num} to end this context, or ^0 <desc> to create a new one."
143
- )
144
- return CaretCommand(ends=[], select=num, new_context_desc=None, remaining_prompt=remaining), None
145
-
146
- # Parse chained commands: E<N>, S<N>, etc.
147
- ends = []
148
- select = None
149
- pos = 0
150
-
151
- while pos < len(command_str):
152
- if command_str[pos].upper() == 'E':
153
- # End command
154
- pos += 1
155
- # Check for wildcard (E*) - end all contexts
156
- if pos < len(command_str) and command_str[pos] == '*':
157
- pos += 1
158
- if len(contexts) == 0:
159
- return None, "No contexts to end."
160
- # Add all context numbers to ends list
161
- for i in range(1, len(contexts) + 1):
162
- if i not in ends:
163
- ends.append(i)
164
- else:
165
- # Read number
166
- num_start = pos
167
- while pos < len(command_str) and command_str[pos].isdigit():
168
- pos += 1
169
- if num_start == pos:
170
- return None, f"Expected number or '*' after 'E' at position {num_start + 1}"
171
- num = int(command_str[num_start:pos])
172
- if num < 1 or num > len(contexts):
173
- if len(contexts) == 0:
174
- return None, "No contexts to end."
175
- return None, f"Context ^E{num} invalid. Choose 1-{len(contexts)}."
176
-
177
- # Check for + suffix meaning "this and all after"
178
- if pos < len(command_str) and command_str[pos] == '+':
179
- pos += 1
180
- # Add num and all higher numbers (older contexts)
181
- for i in range(num, len(contexts) + 1):
182
- if i not in ends:
183
- ends.append(i)
184
- else:
185
- ends.append(num)
186
-
187
- elif command_str[pos].upper() == 'S':
188
- # Select command
189
- pos += 1
190
- # Read number
191
- num_start = pos
192
- while pos < len(command_str) and command_str[pos].isdigit():
193
- pos += 1
194
- if num_start == pos:
195
- return None, f"Expected number after 'S' at position {num_start + 1}"
196
- num = int(command_str[num_start:pos])
197
- if num < 1 or num > len(contexts):
198
- if len(contexts) == 0:
199
- return None, "No contexts to select."
200
- return None, f"Context ^S{num} invalid. Choose 1-{len(contexts)}."
201
- # Validate context is in "implementing" mode
202
- ctx = contexts[num - 1]
203
- if not ctx.in_flight or ctx.in_flight.mode != "implementing":
204
- mode = ctx.in_flight.mode if ctx.in_flight else "none"
205
- return None, (
206
- f"Cannot select context {num} ({ctx.id}) - mode is '{mode}'.\n"
207
- f"Only contexts in 'implementing' mode can be selected.\n"
208
- f"Use ^E{num} to end this context, or ^0 <desc> to create a new one."
209
- )
210
- # Only first S counts
211
- if select is None:
212
- select = num
213
-
214
- else:
215
- return None, (
216
- f"Invalid command '{command_str[pos]}' at position {pos + 1}.\n"
217
- f"Use E<N> to end, E<N>+ to end N and after, E* to end all, S<N> to select.\n"
218
- f"Example: ^E1S2 (end 1, select 2), ^E2+ (end 2 and older), ^E* (end all)"
219
- )
220
-
221
- # Validate: can't select a context that's being ended
222
- if select is not None and select in ends:
223
- return None, f"Cannot select context {select} because it's being ended."
224
-
225
- return CaretCommand(ends=ends, select=select, new_context_desc=None, remaining_prompt=remaining), None
226
-
227
-
228
- class BlockRequest(Exception):
229
- """Raised when the request should be blocked with a message to the user."""
230
- def __init__(self, message: str):
231
- self.message = message
232
- super().__init__(message)
233
-
234
-
235
- def format_context_picker_stderr(contexts: List[Context]) -> str:
236
- """
237
- Format context picker for stderr output (visible to user when blocking).
238
-
239
- Args:
240
- contexts: Available contexts to choose from
241
-
242
- Returns:
243
- Formatted picker message
244
- """
245
- lines = [
246
- "",
247
- "+----------------------------------------------------------------+",
248
- "| CONTEXT SELECTION REQUIRED |",
249
- "+----------------------------------------------------------------+",
250
- ]
251
-
252
- implementing_count = 0
253
- for i, ctx in enumerate(contexts, 1):
254
- time_str = format_relative_time(ctx.last_active)
255
-
256
- # Check if context is in implementing mode (selectable)
257
- is_implementing = ctx.in_flight and ctx.in_flight.mode == "implementing"
258
- if is_implementing:
259
- implementing_count += 1
260
-
261
- # Add status indicator for in-flight work
262
- status = ""
263
- if ctx.in_flight and ctx.in_flight.mode != "none":
264
- mode_display = get_mode_display(ctx.in_flight.mode)
265
- if mode_display:
266
- status = f" {mode_display}"
267
-
268
- # Truncate summary for display
269
- summary = ctx.summary[:45] + "..." if len(ctx.summary) > 48 else ctx.summary
270
-
271
- # Show selectable indicator
272
- selectable = " [selectable]" if is_implementing else " [end only]"
273
- lines.append(f"| ^{i} {ctx.id}{status}{selectable}")
274
- lines.append(f"| {summary}")
275
- lines.append(f"| [{time_str}]")
276
- lines.append("|")
277
-
278
- lines.extend([
279
- "+----------------------------------------------------------------+",
280
- "| Usage: |",
281
- "| ^S<N> - Select context (implementing only) |",
282
- "| ^E<N> - End/complete context |",
283
- "| ^E<N>+ - End context N and all after |",
284
- "| ^E* - End ALL contexts |",
285
- "| ^E1E2S3 - End #1 and #2, select #3 |",
286
- "| ^0 work description - Create new context (10+ chars) |",
287
- "+----------------------------------------------------------------+",
288
- ])
289
-
290
- if implementing_count == 0:
291
- lines.extend([
292
- "| NOTE: No contexts in 'implementing' mode. |",
293
- "| Use ^E<N> to end old contexts, then ^0 to create new. |",
294
- "+----------------------------------------------------------------+",
295
- ])
296
-
297
- lines.append("")
298
-
299
- return "\n".join(lines)
300
-
301
-
302
- def format_command_feedback(ended_contexts: List[Context], selected_context: Optional[Context]) -> str:
303
- """
304
- Format feedback about what context operations were performed.
305
-
306
- Args:
307
- ended_contexts: Contexts that were ended/completed
308
- selected_context: Context that was selected (if any)
309
-
310
- Returns:
311
- Formatted feedback message
312
- """
313
- lines = []
314
-
315
- if ended_contexts:
316
- lines.append("## Contexts Ended")
317
- lines.append("")
318
- for ctx in ended_contexts:
319
- lines.append(f"- **{ctx.id}**: {ctx.summary[:50]}{'...' if len(ctx.summary) > 50 else ''}")
320
- lines.append("")
321
-
322
- if selected_context:
323
- lines.append(f"## Active Context: {selected_context.id}")
324
- lines.append("")
325
- lines.append(f"**Summary:** {selected_context.summary}")
326
-
327
- # Build mode display
328
- mode_display = "Active"
329
- if selected_context.in_flight and selected_context.in_flight.mode != "none":
330
- mode_str = get_mode_display(selected_context.in_flight.mode)
331
- if mode_str:
332
- mode_display = mode_str.strip("[]")
333
-
334
- time_str = format_relative_time(selected_context.last_active)
335
- lines.append(f"**Mode:** {mode_display}")
336
- lines.append(f"**Last Active:** {time_str}")
337
- lines.append("")
338
- lines.append(f'All work belongs to context "{selected_context.id}".')
339
- lines.append("Tasks created with TaskCreate will be persisted to this context.")
340
-
341
- return "\n".join(lines)
342
-
343
-
344
- def determine_context(
345
- user_prompt: str,
346
- project_root: Path = None,
347
- session_id: str = None
348
- ) -> Tuple[Optional[str], str, Optional[str]]:
349
- """
350
- Determine which context this prompt belongs to.
351
-
352
- Returns:
353
- Tuple of:
354
- - context_id: Context ID or None if selection needed
355
- - method: How context was determined (session_match, in_flight, caret_select,
356
- auto_created, single_context, blocked)
357
- - output: System reminder to inject, or None
358
-
359
- Raises:
360
- BlockRequest: When request should be blocked to show picker to user
361
- """
362
- # 0. Skip context creation for internal subprocess calls (orchestrator, agents)
363
- if is_internal_call():
364
- eprint("[context_enforcer] Skipping: internal subprocess call")
365
- return (None, "skip_internal", None)
366
-
367
- # 1. Check if session already belongs to a context (HIGHEST PRIORITY)
368
- # This prevents context switching on subsequent prompts - one context per session
369
- if session_id:
370
- session_context = get_context_by_session_id(session_id, project_root)
371
- if session_context:
372
- eprint(f"[context_enforcer] Session already in context: {session_context.id}")
373
- return (
374
- session_context.id,
375
- "session_match",
376
- format_active_context_reminder(session_context, project_root)
377
- )
378
-
379
- # 2. Check for bare "^" - show context picker
380
- if user_prompt.strip() == "^":
381
- contexts = get_all_contexts(status="active", project_root=project_root)
382
- if not contexts:
383
- raise BlockRequest(
384
- "No contexts exist.\n\n"
385
- "Just type your task to start a new context.\n"
386
- "Example: implement user authentication system"
387
- )
388
- raise BlockRequest(format_context_picker_stderr(contexts))
389
-
390
- # 3. Check for explicit caret commands (^E, ^S, ^0, ^N)
391
- if user_prompt.startswith("^"):
392
- contexts = get_all_contexts(status="active", project_root=project_root)
393
- return _handle_caret_command(user_prompt, contexts, project_root)
394
-
395
- # 4. No caret prefix - check in-flight contexts for auto-selection
396
- in_flight_contexts = get_all_in_flight_contexts(project_root)
397
-
398
- if len(in_flight_contexts) == 0:
399
- # No in-flight work - auto-create new context from prompt
400
-
401
- # Skip auto-creation for certain prompts that don't represent work
402
- skip_patterns = [
403
- "/help", "/clear", "/status", "hello", "hi", "hey",
404
- "thanks", "thank you", "bye", "goodbye"
405
- ]
406
- prompt_lower = user_prompt.lower().strip()
407
-
408
- # Don't auto-create for greetings or help commands
409
- if any(prompt_lower.startswith(p) or prompt_lower == p for p in skip_patterns):
410
- return (None, "no_context_needed", None)
411
-
412
- # Auto-create context from prompt
413
- try:
414
- new_context = create_context_from_prompt(user_prompt, project_root)
415
- # Set to implementing mode so it can be selected
416
- update_plan_status(new_context.id, "implementing", project_root=project_root)
417
- new_context.in_flight.mode = "implementing" # Update local copy for display
418
- eprint(f"[context_enforcer] Auto-created new context: {new_context.id}")
419
- return (
420
- new_context.id,
421
- "auto_created",
422
- format_context_created(new_context)
423
- )
424
- except Exception as e:
425
- eprint(f"[context_enforcer] Failed to create context: {e}")
426
- return (None, "creation_failed", None)
427
-
428
- elif len(in_flight_contexts) == 1:
429
- # Single in-flight context - auto-select it
430
- ctx = in_flight_contexts[0]
431
- mode = ctx.in_flight.mode if ctx.in_flight else "none"
432
- eprint(f"[context_enforcer] Auto-selected single in-flight context: {ctx.id} (mode={mode})")
433
-
434
- # Use mode-specific formatter for better continuation context
435
- if mode == "pending_implementation":
436
- output = format_pending_plan_continuation(ctx, project_root)
437
- elif mode == "implementing":
438
- output = format_implementation_continuation(ctx, project_root)
439
- else:
440
- output = format_active_context_reminder(ctx, project_root, include_restore=True)
441
-
442
- return (ctx.id, "auto_selected", output)
443
-
444
- else:
445
- # Multiple in-flight contexts - block and show picker
446
- eprint(f"[context_enforcer] Multiple in-flight contexts ({len(in_flight_contexts)}), showing picker")
447
- raise BlockRequest(
448
- f"Multiple contexts have in-flight work ({len(in_flight_contexts)} active).\n"
449
- "Select one to continue, or use ^ to see all contexts:\n" +
450
- format_context_picker_stderr(in_flight_contexts)
451
- )
452
-
453
-
454
- def _handle_caret_command(
455
- user_prompt: str,
456
- contexts: List[Context],
457
- project_root: Path
458
- ) -> Tuple[Optional[str], str, Optional[str]]:
459
- """
460
- Handle explicit caret commands (^E, ^S, ^0, ^N).
461
-
462
- Args:
463
- user_prompt: User's prompt starting with ^
464
- contexts: List of active contexts
465
- project_root: Project root directory
466
-
467
- Returns:
468
- Tuple of (context_id, method, output)
469
-
470
- Raises:
471
- BlockRequest: When command is invalid or selection needed
472
- """
473
- # No contexts case - only ^0 is valid
474
- if not contexts:
475
- match = re.match(r'^\^(\S+)(?:\s+(.*))?$', user_prompt, re.DOTALL)
476
- if not match:
477
- raise BlockRequest(
478
- "Invalid prefix. Use ^0 <description> to create a new context.\n"
479
- "Example: ^0 implement user authentication system"
480
- )
481
-
482
- prefix_value = match.group(1)
483
- remaining = match.group(2) or ""
484
-
485
- # Must be ^0 for new context
486
- if not prefix_value.isdigit() or int(prefix_value) != 0:
487
- raise BlockRequest(
488
- f"No existing contexts to select. Use ^0 <description> to create a new context.\n"
489
- f"Example: ^0 implement user authentication system"
490
- )
491
-
492
- description = remaining.strip()
493
- if len(description) < MIN_NEW_CONTEXT_CHARS:
494
- raise BlockRequest(
495
- f"Please provide a longer description for your new context.\n"
496
- f"Your description '{description}' is only {len(description)} characters.\n"
497
- f"Minimum required: {MIN_NEW_CONTEXT_CHARS} characters.\n"
498
- f"Example: ^0 implement user authentication with JWT tokens"
499
- )
500
- try:
501
- new_context = create_context_from_prompt(description, project_root)
502
- update_plan_status(new_context.id, "implementing", project_root=project_root)
503
- new_context.in_flight.mode = "implementing"
504
- eprint(f"[context_enforcer] Created context from ^0: {new_context.id}")
505
- return (
506
- new_context.id,
507
- "caret_new",
508
- format_context_created(new_context)
509
- )
510
- except Exception as e:
511
- eprint(f"[context_enforcer] Failed to create context: {e}")
512
- raise BlockRequest(f"Failed to create context: {e}")
513
-
514
- # Parse caret commands
515
- cmd, error = parse_chained_caret(user_prompt, contexts)
516
-
517
- if error:
518
- raise BlockRequest(error + "\n" + format_context_picker_stderr(contexts))
519
-
520
- if not cmd:
521
- # Should not happen - user_prompt starts with ^ but didn't parse
522
- raise BlockRequest(format_context_picker_stderr(contexts))
523
-
524
- # Process chained commands
525
- ended_contexts = []
526
-
527
- # End specified contexts
528
- for end_num in cmd.ends:
529
- ctx_to_end = contexts[end_num - 1] # 1-indexed
530
- complete_context(ctx_to_end.id, project_root)
531
- ended_contexts.append(ctx_to_end)
532
- eprint(f"[context_enforcer] Ended context: {ctx_to_end.id}")
533
-
534
- # Handle new context creation
535
- if cmd.new_context_desc:
536
- try:
537
- new_context = create_context_from_prompt(cmd.new_context_desc, project_root)
538
- update_plan_status(new_context.id, "implementing", project_root=project_root)
539
- new_context.in_flight.mode = "implementing"
540
- eprint(f"[context_enforcer] Created context from ^0: {new_context.id}")
541
- output = format_command_feedback(ended_contexts, new_context)
542
- return (
543
- new_context.id,
544
- "caret_new",
545
- output
546
- )
547
- except Exception as e:
548
- eprint(f"[context_enforcer] Failed to create context: {e}")
549
- raise BlockRequest(f"Failed to create context: {e}")
550
-
551
- # Handle context selection
552
- if cmd.select:
553
- selected_ctx = contexts[cmd.select - 1] # 1-indexed
554
- eprint(f"[context_enforcer] Caret-selected context: {selected_ctx.id}")
555
- output = format_command_feedback(ended_contexts, selected_ctx)
556
- return (
557
- selected_ctx.id,
558
- "caret_select",
559
- output
560
- )
561
-
562
- # Only ended contexts, no selection - refresh context list and block
563
- if ended_contexts:
564
- remaining_contexts = get_all_contexts(status="active", project_root=project_root)
565
- feedback = format_command_feedback(ended_contexts, None)
566
- if not remaining_contexts:
567
- raise BlockRequest(
568
- feedback + "\n" +
569
- "All contexts have been ended. No context selected.\n\n"
570
- "Just type your task to start a new context.\n"
571
- "Example: implement user authentication system"
572
- )
573
- raise BlockRequest(
574
- feedback + "\n" +
575
- "No context selected.\n\n" +
576
- "Select a context to continue:\n" +
577
- format_context_picker_stderr(remaining_contexts)
578
- )
579
-
580
- # Parsed but nothing to do - shouldn't happen
581
- raise BlockRequest(format_context_picker_stderr(contexts))
582
-
583
-
584
- def main():
585
- """
586
- Standalone entry point for testing.
587
-
588
- In production, use user_prompt_submit.py as the unified entry point.
589
- """
590
- try:
591
- input_data = sys.stdin.read().strip()
592
- if not input_data:
593
- return
594
-
595
- hook_input = json.loads(input_data)
596
- user_prompt = hook_input.get("prompt", "")
597
- if not user_prompt:
598
- return
599
-
600
- project_root = project_dir(hook_input)
601
-
602
- try:
603
- context_id, method, output = determine_context(user_prompt, project_root)
604
- eprint(f"[context_enforcer] Method: {method}, Context: {context_id}")
605
-
606
- if output:
607
- print(output)
608
-
609
- except BlockRequest as e:
610
- # Block the request - print to stderr and exit with code 2
611
- print(e.message, file=sys.stderr)
612
- sys.exit(2)
613
-
614
- except Exception as e:
615
- eprint(f"[context_enforcer] ERROR: {e}")
616
- import traceback
617
- eprint(traceback.format_exc())
618
-
619
-
620
- if __name__ == "__main__":
621
- main()
622
-
623
-
624
- # Export for use by unified hook
625
- __all__ = ["determine_context", "BlockRequest"]