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