aiwcli 0.10.3 → 0.11.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 (191) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +107 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/git-state.ts +1 -1
  24. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  25. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  26. package/dist/templates/_shared/lib-ts/base/logger.ts +15 -2
  27. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  28. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  29. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +142 -0
  30. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  31. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  32. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  33. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  34. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +43 -23
  35. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  36. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  37. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +158 -0
  38. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  39. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  40. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  41. package/dist/templates/_shared/scripts/resume_handoff.ts +345 -0
  42. package/dist/templates/_shared/scripts/save_handoff.ts +3 -3
  43. package/dist/templates/_shared/scripts/status_line.ts +687 -0
  44. package/dist/templates/cc-native/.claude/settings.json +175 -185
  45. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  46. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  47. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  48. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1027 -0
  50. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +792 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +115 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +120 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +250 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +275 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +107 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +240 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  70. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +385 -0
  71. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  72. package/dist/templates/cc-native/_cc-native/plan-review.config.json +14 -1
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +2 -2
  75. package/dist/templates/_shared/hooks/__init__.py +0 -16
  76. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  87. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  88. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  89. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  90. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  91. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  92. package/dist/templates/_shared/hooks/session_end.py +0 -173
  93. package/dist/templates/_shared/hooks/session_start.py +0 -206
  94. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  95. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  96. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  97. package/dist/templates/_shared/lib/__init__.py +0 -1
  98. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  100. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  108. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  109. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  110. package/dist/templates/_shared/lib/base/constants.py +0 -358
  111. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  112. package/dist/templates/_shared/lib/base/inference.py +0 -307
  113. package/dist/templates/_shared/lib/base/logger.py +0 -305
  114. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  115. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  116. package/dist/templates/_shared/lib/base/utils.py +0 -263
  117. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  118. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  130. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  131. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  132. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  133. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  134. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  135. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  136. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  137. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  138. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  139. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  140. package/dist/templates/_shared/lib/templates/README.md +0 -206
  141. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  142. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  145. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  146. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  147. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  148. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  149. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  150. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  151. package/dist/templates/_shared/scripts/status_line.py +0 -716
  152. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  153. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  154. package/dist/templates/cc-native/MIGRATION.md +0 -86
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  160. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  161. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  162. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  163. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  164. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  165. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  166. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  174. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  175. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  176. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  187. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  188. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  189. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  190. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  191. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -1,508 +0,0 @@
1
- """Context selection module - determines which context a prompt belongs to.
2
-
3
- Single entry point: determine_context(prompt, session_id, project_root)
4
- Returns (context_id, method, output_text).
5
-
6
- Selection priority:
7
- 1. session_match - session_id found in index.json sessions map
8
- 2. caret_command - prompt starts with ^ -> parse and execute
9
- 3. plan_content_match - FALLBACK: match against has_plan contexts via hash/signature
10
- 3b. handoff_match - FALLBACK: match against has_handoff contexts
11
- 4. default - create new context
12
-
13
- Note: The primary plan/handoff restore path is now session_start.py which handles
14
- SessionStart(source=clear). It finds has_plan/has_handoff contexts and binds the new
15
- session before UserPromptSubmit fires. Cases 3/3b here are fallbacks for edge
16
- cases where session_start didn't consume the staged state (e.g., startup/resume).
17
- """
18
- import hashlib
19
- import re
20
- from dataclasses import dataclass
21
- from pathlib import Path
22
- from typing import List, Optional, Tuple
23
-
24
- from .context_store import (
25
- ContextState,
26
- get_context,
27
- get_all_contexts,
28
- get_context_by_session_id,
29
- create_context_from_prompt,
30
- create_context,
31
- complete_context,
32
- bind_session,
33
- update_mode,
34
- )
35
- from .context_formatter import (
36
- format_active_context_reminder,
37
- format_context_created,
38
- format_context_picker_stderr,
39
- format_command_feedback,
40
- format_handoff_continuation,
41
- format_plan_continuation,
42
- format_active_continuation,
43
- )
44
- from .plan_manager import normalize_plan_content
45
- from ..base.subprocess_utils import is_internal_call
46
- from ..base.logger import log_debug, log_info, log_warn, log_error
47
-
48
- # Minimum characters required for new context description
49
- MIN_NEW_CONTEXT_CHARS = 10
50
-
51
-
52
- class BlockRequest(Exception):
53
- """Raised when the request should be blocked with a message to user."""
54
- def __init__(self, message: str):
55
- self.message = message
56
- super().__init__(message)
57
-
58
-
59
- @dataclass
60
- class CaretCommand:
61
- """Parsed caret command result."""
62
- ends: List[str] # Context IDs to end (race-safe)
63
- select: Optional[str] # Context ID to select (race-safe)
64
- new_context_desc: Optional[str] # Description for new context (^0)
65
- remaining_prompt: str # The remaining prompt after the command
66
-
67
-
68
- def resolve_context_by_prefix(query: str, contexts: List[ContextState]) -> Tuple[Optional[int], Optional[str]]:
69
- """Resolve a context ID query to an index (1-based) using tiered matching.
70
-
71
- Match priority: exact > prefix > substring (all case-insensitive).
72
- Returns (index, None) on unique match, (None, error) on 0 or 2+ matches.
73
- """
74
- q = query.lower()
75
- available = ', '.join(c.id for c in contexts)
76
-
77
- # Tier 1: Exact match
78
- exact = [(i, ctx) for i, ctx in enumerate(contexts, 1) if ctx.id.lower() == q]
79
- if len(exact) == 1:
80
- return exact[0][0], None
81
-
82
- # Tier 2: Prefix match
83
- prefix = [(i, ctx) for i, ctx in enumerate(contexts, 1) if ctx.id.lower().startswith(q)]
84
- if len(prefix) == 1:
85
- return prefix[0][0], None
86
- if len(prefix) > 1:
87
- return None, f"Ambiguous match '{query}' — {len(prefix)} prefix matches: {', '.join(c.id for _, c in prefix)}. Be more specific."
88
-
89
- # Tier 3: Substring match
90
- substr = [(i, ctx) for i, ctx in enumerate(contexts, 1) if q in ctx.id.lower()]
91
- if len(substr) == 1:
92
- return substr[0][0], None
93
- if len(substr) > 1:
94
- return None, f"Ambiguous match '{query}' — {len(substr)} substring matches: {', '.join(c.id for _, c in substr)}. Be more specific."
95
-
96
- return None, f"No context matches '{query}'. Available: {available}"
97
-
98
-
99
- def parse_chained_caret(prompt: str, contexts: List[ContextState]) -> Tuple[Optional[CaretCommand], Optional[str]]:
100
- """Parse chained caret commands from user prompt.
101
-
102
- Syntax:
103
- - ^E<N>: End context N
104
- - ^E<N>+: End context N and all after
105
- - ^E*: End ALL contexts
106
- - ^S<N>: Select context N
107
- - ^0 <desc>: Create new context
108
- - ^<N>: Shorthand for ^S<N>
109
- - ^E:query / ^S:query: End/select by ID prefix match (race-safe)
110
- - Chain: ^E1E2S3 means end 1, end 2, select 3
111
- """
112
- if not prompt.startswith("^"):
113
- return None, None
114
-
115
- match = re.match(r'^\^(\S+)(?:\s+(.*))?$', prompt, re.DOTALL)
116
- if not match:
117
- return None, "Invalid prefix. Use ^E<N> to end, ^S<N> to select, or ^0 <desc> for new context."
118
-
119
- command_str = match.group(1)
120
- remaining = (match.group(2) or "").strip()
121
-
122
- # ^N shorthand
123
- if command_str.isdigit():
124
- num = int(command_str)
125
- if num == 0:
126
- if len(remaining) < MIN_NEW_CONTEXT_CHARS:
127
- return None, (
128
- f"Please provide a longer description for your new context.\n"
129
- f"Your description '{remaining}' is only {len(remaining)} characters.\n"
130
- f"Minimum required: {MIN_NEW_CONTEXT_CHARS} characters.\n"
131
- f"Example: ^0 implement user authentication with JWT tokens"
132
- )
133
- return CaretCommand(ends=[], select=None, new_context_desc=remaining, remaining_prompt=""), None
134
- else:
135
- if num < 1 or num > len(contexts):
136
- if not contexts:
137
- return None, "No existing contexts. Use ^0 <description> to create a new one."
138
- return None, f"Invalid selection. Choose 1-{len(contexts)} for existing contexts, or ^0 for new."
139
- ctx = contexts[num - 1]
140
- return CaretCommand(ends=[], select=ctx.id, new_context_desc=None, remaining_prompt=remaining), None
141
-
142
- # Parse chained commands
143
- ends = []
144
- select = None
145
- pos = 0
146
-
147
- while pos < len(command_str):
148
- if command_str[pos].upper() == 'E':
149
- pos += 1
150
- if pos < len(command_str) and command_str[pos] == '*':
151
- pos += 1
152
- if not contexts:
153
- return None, "No contexts to end."
154
- for ctx in contexts:
155
- if ctx.id not in ends:
156
- ends.append(ctx.id)
157
- elif pos < len(command_str) and command_str[pos] == ':':
158
- pos += 1
159
- prefix_start = pos
160
- while pos < len(command_str) and command_str[pos] not in ('E', 'S', 'e', 's'):
161
- pos += 1
162
- pfx = command_str[prefix_start:pos]
163
- if not pfx:
164
- return None, "Expected ID query after 'E:'"
165
- idx, err = resolve_context_by_prefix(pfx, contexts)
166
- if err:
167
- return None, err
168
- ctx = contexts[idx - 1]
169
- if ctx.id not in ends:
170
- ends.append(ctx.id)
171
- else:
172
- num_start = pos
173
- while pos < len(command_str) and command_str[pos].isdigit():
174
- pos += 1
175
- if num_start == pos:
176
- return None, f"Expected number, '*', or ':prefix' after 'E' at position {num_start + 1}"
177
- num = int(command_str[num_start:pos])
178
- if num < 1 or num > len(contexts):
179
- if not contexts:
180
- return None, "No contexts to end."
181
- return None, f"Context ^E{num} invalid. Choose 1-{len(contexts)}."
182
- if pos < len(command_str) and command_str[pos] == '+':
183
- pos += 1
184
- for i in range(num, len(contexts) + 1):
185
- ctx = contexts[i - 1]
186
- if ctx.id not in ends:
187
- ends.append(ctx.id)
188
- else:
189
- ctx = contexts[num - 1]
190
- if ctx.id not in ends:
191
- ends.append(ctx.id)
192
-
193
- elif command_str[pos].upper() == 'S':
194
- pos += 1
195
- if pos < len(command_str) and command_str[pos] == ':':
196
- pos += 1
197
- prefix_start = pos
198
- while pos < len(command_str) and command_str[pos] not in ('E', 'S', 'e', 's'):
199
- pos += 1
200
- pfx = command_str[prefix_start:pos]
201
- if not pfx:
202
- return None, "Expected ID query after 'S:'"
203
- idx, err = resolve_context_by_prefix(pfx, contexts)
204
- if err:
205
- return None, err
206
- ctx = contexts[idx - 1]
207
- else:
208
- num_start = pos
209
- while pos < len(command_str) and command_str[pos].isdigit():
210
- pos += 1
211
- if num_start == pos:
212
- return None, f"Expected number or ':prefix' after 'S' at position {num_start + 1}"
213
- num = int(command_str[num_start:pos])
214
- if num < 1 or num > len(contexts):
215
- if not contexts:
216
- return None, "No contexts to select."
217
- return None, f"Context ^S{num} invalid. Choose 1-{len(contexts)}."
218
- ctx = contexts[num - 1]
219
- if select is None:
220
- select = ctx.id
221
-
222
- else:
223
- return None, (
224
- f"Invalid command '{command_str[pos]}' at position {pos + 1}.\n"
225
- f"Use E<N> to end, E<N>+ to end N and after, E* to end all, S<N> to select.\n"
226
- f"Example: ^E1S2 (end 1, select 2), ^E2+ (end 2 and older), ^E* (end all)"
227
- )
228
-
229
- if select is not None and select in ends:
230
- return None, f"Cannot select context '{select}' because it's being ended."
231
-
232
- return CaretCommand(ends=ends, select=select, new_context_desc=None, remaining_prompt=remaining), None
233
-
234
-
235
- # ---------------------------------------------------------------------------
236
- # Plan content matching (fallback — primary path is session_start.py)
237
- # ---------------------------------------------------------------------------
238
-
239
- def _match_plan_content(prompt: str, has_plan_contexts: List[ContextState]) -> Optional[ContextState]:
240
- """Fallback plan matching for edge cases where session_start didn't consume has_plan.
241
-
242
- The primary plan restore path is session_start.py (SessionStart source=clear).
243
- This fallback handles cases like startup/resume where has_plan persisted.
244
-
245
- Tiers (cascading):
246
- 1. Embedded plan-id (HTML comment)
247
- 2. Normalized content hash
248
- 3. Multi-anchor signature
249
- 4. Legacy signature fallback
250
- """
251
- if not has_plan_contexts:
252
- return None
253
-
254
- # Tier 1: Plan ID match (most reliable)
255
- id_match = re.search(r'<!-- plan-id: ([a-f0-9]+) -->', prompt)
256
- if id_match:
257
- found_id = id_match.group(1)
258
- for ctx in has_plan_contexts:
259
- if getattr(ctx, 'plan_id', None) == found_id:
260
- log_debug("context_selector", f"Tier 1 plan-id match: {ctx.id} (id: {found_id})")
261
- return ctx
262
-
263
- # Tier 2: Normalized hash match
264
- normalized = normalize_plan_content(prompt)
265
- norm_hash = hashlib.sha256(normalized.encode('utf-8')).hexdigest()[:12]
266
- for ctx in has_plan_contexts:
267
- if ctx.plan_hash and ctx.plan_hash == norm_hash:
268
- log_debug("context_selector", f"Tier 2 normalized hash match: {ctx.id} (hash: {norm_hash})")
269
- return ctx
270
-
271
- # Tier 3: Multi-anchor signature match
272
- for ctx in has_plan_contexts:
273
- anchors = getattr(ctx, 'plan_anchors', [])
274
- if anchors:
275
- hits = sum(1 for a in anchors if a in prompt)
276
- if hits >= 2 and hits >= len(anchors) // 2:
277
- log_debug("context_selector", f"Tier 3 anchor match: {ctx.id} ({hits}/{len(anchors)} anchors)")
278
- return ctx
279
-
280
- # Tier 4 (legacy fallback): Signature match for pre-upgrade contexts
281
- prompt_head = prompt[:500]
282
- for ctx in has_plan_contexts:
283
- if ctx.plan_signature and ctx.plan_signature in prompt_head:
284
- log_debug("context_selector", f"Tier 4 legacy signature match: {ctx.id}")
285
- return ctx
286
-
287
- # No match — let caller fall through to new context creation
288
- return None
289
-
290
-
291
- # ---------------------------------------------------------------------------
292
- # Context creation helper
293
- # ---------------------------------------------------------------------------
294
-
295
- def _create_new_context(prompt: str, project_root: Path) -> Tuple[Optional[str], str, Optional[str]]:
296
- """Create a new context from the user's prompt (case 5: default)."""
297
- try:
298
- new_ctx = create_context_from_prompt(prompt, project_root)
299
- update_mode(new_ctx.id, "active", project_root=project_root)
300
- new_ctx.mode = "active"
301
- log_info("context_selector", f"Auto-created context: {new_ctx.id}")
302
- return (new_ctx.id, "auto_created", format_context_created(new_ctx))
303
- except Exception as e:
304
- log_error("context_selector", f"Primary context creation failed: {e}")
305
- try:
306
- from datetime import datetime
307
- fallback_id = datetime.now().strftime("%y%m%d-%H%M") + "-context"
308
- new_ctx = create_context(
309
- context_id=fallback_id,
310
- summary=prompt.strip()[:200] or "New context",
311
- method="auto-created-fallback",
312
- tags=["auto-created", "fallback"],
313
- project_root=project_root,
314
- )
315
- update_mode(new_ctx.id, "active", project_root=project_root)
316
- new_ctx.mode = "active"
317
- log_info("context_selector", f"Fallback context created: {new_ctx.id}")
318
- return (new_ctx.id, "auto_created_fallback", format_context_created(new_ctx))
319
- except Exception as e2:
320
- log_error("context_selector", f"ALL context creation failed: {e2}")
321
- return (None, "creation_failed", None)
322
-
323
-
324
- # ---------------------------------------------------------------------------
325
- # Caret command handler
326
- # ---------------------------------------------------------------------------
327
-
328
- def _handle_caret_command(
329
- prompt: str,
330
- contexts: List[ContextState],
331
- project_root: Path,
332
- ) -> Tuple[Optional[str], str, Optional[str]]:
333
- """Handle explicit caret commands (^E, ^S, ^0, ^N).
334
-
335
- Raises:
336
- BlockRequest: When command is invalid or selection needed
337
- """
338
- if not contexts:
339
- match = re.match(r'^\^(\S+)(?:\s+(.*))?$', prompt, re.DOTALL)
340
- if not match:
341
- raise BlockRequest(
342
- "Invalid prefix. Use ^0 <description> to create a new context.\n"
343
- "Example: ^0 implement user authentication system"
344
- )
345
- prefix_value = match.group(1)
346
- remaining = match.group(2) or ""
347
- if not prefix_value.isdigit() or int(prefix_value) != 0:
348
- raise BlockRequest(
349
- "No existing contexts to select. Use ^0 <description> to create a new context.\n"
350
- "Example: ^0 implement user authentication system"
351
- )
352
- description = remaining.strip()
353
- if len(description) < MIN_NEW_CONTEXT_CHARS:
354
- raise BlockRequest(
355
- f"Please provide a longer description for your new context.\n"
356
- f"Your description '{description}' is only {len(description)} characters.\n"
357
- f"Minimum required: {MIN_NEW_CONTEXT_CHARS} characters.\n"
358
- f"Example: ^0 implement user authentication with JWT tokens"
359
- )
360
- return _create_new_context(description, project_root)
361
-
362
- cmd, error = parse_chained_caret(prompt, contexts)
363
- if error:
364
- raise BlockRequest(error + "\n" + format_context_picker_stderr(contexts))
365
- if not cmd:
366
- raise BlockRequest(format_context_picker_stderr(contexts))
367
-
368
- ended_contexts = []
369
- for ctx_id in cmd.ends:
370
- ctx_to_end = next((c for c in contexts if c.id == ctx_id), None)
371
- if ctx_to_end is None:
372
- raise BlockRequest(f"Context '{ctx_id}' no longer exists.\n" + format_context_picker_stderr(contexts))
373
- complete_context(ctx_to_end.id, project_root)
374
- ended_contexts.append(ctx_to_end)
375
- log_info("context_selector", f"Ended context: {ctx_to_end.id}")
376
-
377
- if cmd.new_context_desc:
378
- ctx_id, method, output = _create_new_context(cmd.new_context_desc, project_root)
379
- if ctx_id and ended_contexts:
380
- new_ctx = get_context(ctx_id, project_root)
381
- output = format_command_feedback(ended_contexts, new_ctx)
382
- return (ctx_id, "caret_new" if method != "creation_failed" else method, output)
383
-
384
- if cmd.select:
385
- selected_ctx = next((c for c in contexts if c.id == cmd.select), None)
386
- if selected_ctx is None:
387
- raise BlockRequest(f"Context '{cmd.select}' no longer exists.\n" + format_context_picker_stderr(contexts))
388
- log_info("context_selector", f"Caret-selected context: {selected_ctx.id}")
389
- return (selected_ctx.id, "caret_select", format_command_feedback(ended_contexts, selected_ctx))
390
-
391
- if ended_contexts:
392
- remaining_contexts = get_all_contexts(status="active", project_root=project_root)
393
- feedback = format_command_feedback(ended_contexts, None)
394
- if not remaining_contexts:
395
- raise BlockRequest(
396
- feedback + "\nAll contexts have been ended. No context selected.\n\n"
397
- "Just type your task to start a new context.\n"
398
- "Example: implement user authentication system"
399
- )
400
- raise BlockRequest(
401
- feedback + "\nNo context selected.\n\nSelect a context to continue:\n" +
402
- format_context_picker_stderr(remaining_contexts)
403
- )
404
-
405
- raise BlockRequest(format_context_picker_stderr(contexts))
406
-
407
-
408
- # ---------------------------------------------------------------------------
409
- # Main entry point
410
- # ---------------------------------------------------------------------------
411
-
412
- def determine_context(
413
- prompt: str,
414
- session_id: str = None,
415
- project_root: Path = None,
416
- ) -> Tuple[Optional[str], str, Optional[str]]:
417
- """Determine which context this prompt belongs to.
418
-
419
- Selection priority (5 cases):
420
- 1. session_match - session_id already bound to a context
421
- 2. caret_command - prompt starts with ^, parse and execute
422
- 3. plan_content_match - FALLBACK: match has_plan contexts via hash/signature
423
- 3b. handoff_match - FALLBACK: match has_handoff contexts
424
- 4. default - create new context
425
-
426
- Note: The primary plan/handoff restore is handled by session_start.py on
427
- SessionStart(source=clear), which binds the session before this runs.
428
- Cases 3/3b are fallbacks for edge cases.
429
-
430
- Returns:
431
- (context_id, method, output_text)
432
-
433
- Raises:
434
- BlockRequest: When request should be blocked to show picker
435
- """
436
- if is_internal_call():
437
- log_debug("context_selector", "Skipping: internal subprocess call")
438
- return (None, "skip_internal", None)
439
-
440
- # --- Case 1: session_match ---
441
- if session_id:
442
- session_context = get_context_by_session_id(session_id, project_root)
443
- if session_context:
444
- log_info("context_selector", f"Session match: {session_context.id}")
445
- return (
446
- session_context.id,
447
- "session_match",
448
- format_active_context_reminder(session_context, project_root),
449
- )
450
-
451
- # --- Case 2: caret_command ---
452
- if prompt.strip() == "^":
453
- contexts = get_all_contexts(status="active", project_root=project_root)
454
- if not contexts:
455
- raise BlockRequest(
456
- "No contexts exist.\n\nJust type your task to start a new context.\n"
457
- "Example: implement user authentication system"
458
- )
459
- raise BlockRequest(format_context_picker_stderr(contexts))
460
-
461
- if prompt.startswith("^"):
462
- contexts = get_all_contexts(status="active", project_root=project_root)
463
- return _handle_caret_command(prompt, contexts, project_root)
464
-
465
- # --- Case 3: plan_content_match (fallback — primary path is session_start.py) ---
466
- has_plan_contexts = [
467
- c for c in get_all_contexts(status="active", project_root=project_root)
468
- if c.mode == "has_plan"
469
- ]
470
-
471
- if has_plan_contexts:
472
- matched = _match_plan_content(prompt, has_plan_contexts)
473
- if matched:
474
- if session_id:
475
- bind_session(matched.id, session_id, project_root)
476
-
477
- update_mode(matched.id, "active", project_root=project_root, plan_consumed=True)
478
- matched.mode = "active"
479
-
480
- log_info("context_selector", f"Plan match (fallback): {matched.id}")
481
- return (matched.id, "plan_content_match", format_plan_continuation(matched, project_root))
482
-
483
- # --- Case 3b: handoff_match (fallback — primary path is session_start.py) ---
484
- has_handoff_contexts = [
485
- c for c in get_all_contexts(status="active", project_root=project_root)
486
- if c.mode == "has_handoff"
487
- ]
488
-
489
- if has_handoff_contexts:
490
- target = has_handoff_contexts[0] # most recent
491
- if session_id:
492
- bind_session(target.id, session_id, project_root)
493
- update_mode(target.id, "active", project_root=project_root, handoff_consumed=True)
494
- target.mode = "active"
495
- log_info("context_selector", f"Handoff match (fallback): {target.id}")
496
- return (target.id, "handoff_match", format_handoff_continuation(target, project_root))
497
-
498
- # --- Case 4: default ---
499
- return _create_new_context(prompt, project_root)
500
-
501
-
502
- __all__ = [
503
- "determine_context",
504
- "BlockRequest",
505
- "CaretCommand",
506
- "resolve_context_by_prefix",
507
- "parse_chained_caret",
508
- ]