aiwcli 0.12.6 → 0.12.7

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 (124) hide show
  1. package/bin/dev.cmd +3 -3
  2. package/bin/dev.js +16 -16
  3. package/bin/run.cmd +3 -3
  4. package/bin/run.js +21 -21
  5. package/dist/commands/branch.js +7 -2
  6. package/dist/lib/bmad-installer.js +37 -37
  7. package/dist/lib/terminal.d.ts +2 -0
  8. package/dist/lib/terminal.js +57 -7
  9. package/dist/templates/CLAUDE.md +205 -205
  10. package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -12
  11. package/dist/templates/_shared/.claude/commands/handoff.md +12 -12
  12. package/dist/templates/_shared/.claude/settings.json +65 -65
  13. package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
  14. package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
  15. package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -421
  16. package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
  17. package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
  18. package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
  21. package/dist/templates/_shared/handoff-system/workflows/handoff.md +254 -254
  22. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
  23. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
  24. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
  25. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
  26. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
  27. package/dist/templates/_shared/hooks-ts/session_end.ts +196 -196
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
  29. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
  30. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
  31. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
  32. package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
  33. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
  34. package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
  35. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
  36. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
  37. package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
  38. package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
  39. package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -202
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  42. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
  43. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
  44. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
  45. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  46. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  47. package/dist/templates/_shared/lib-ts/package.json +20 -20
  48. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  49. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  50. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  51. package/dist/templates/_shared/lib-ts/types.ts +186 -186
  52. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  53. package/dist/templates/_shared/scripts/status_line.ts +690 -690
  54. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
  55. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
  56. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
  57. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  58. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  59. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  61. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  62. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  63. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  64. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  65. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  66. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
  68. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
  69. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  70. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  71. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  72. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  73. package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
  74. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
  75. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
  76. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
  77. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
  78. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
  79. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
  80. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  82. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  83. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  84. package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
  85. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  86. package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
  87. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
  88. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  89. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
  90. package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
  91. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  92. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  93. package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
  94. package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
  95. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
  96. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
  97. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
  98. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -66
  99. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
  100. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
  101. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -196
  102. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
  103. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
  104. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  105. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  106. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  107. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  108. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  109. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  110. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  111. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
  112. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  113. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  114. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  115. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  116. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  117. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  118. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  119. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
  120. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
  121. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
  122. package/oclif.manifest.json +1 -1
  123. package/package.json +108 -108
  124. package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
@@ -1,524 +1,524 @@
1
- /**
2
- * Context selection — determines which context a prompt belongs to.
3
- * See SPEC.md §8
4
- *
5
- * Single entry point: determineContext(prompt, sessionId, projectRoot)
6
- * Returns [contextId, method, outputText].
7
- *
8
- * Selection priority:
9
- * 1. session_match — session_id found in index.json sessions map
10
- * 2. caret_command — prompt starts with ^ → parse and execute
11
- * 3. plan_content_match — FALLBACK: match against has_plan contexts
12
- * 3b. handoff_match — FALLBACK: match against has_handoff contexts
13
- * 4. default — create new context
14
- */
15
-
16
- import * as crypto from "node:crypto";
17
- import {
18
- getContext,
19
- getAllContexts,
20
- getContextBySessionId,
21
- createContextFromPrompt,
22
- createContext,
23
- completeContext,
24
- bindSession,
25
- updateMode,
26
- determineArtifactType,
27
- } from "./context-store.js";
28
- import {
29
- formatActiveContextReminder,
30
- formatContextCreated,
31
- formatContextPickerStderr,
32
- formatCommandFeedback,
33
- formatHandoffContinuation,
34
- formatPlanContinuation,
35
- formatActiveContinuation,
36
- } from "./context-formatter.js";
37
- import { normalizePlanContent } from "./plan-manager.js";
38
- import { isInternalCall } from "../base/subprocess-utils.js";
39
- import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
40
- import type { ContextState, CaretCommand } from "../types.js";
41
-
42
- /** Minimum characters required for new context description. */
43
- const MIN_NEW_CONTEXT_CHARS = 10;
44
-
45
- /**
46
- * Raised when the request should be blocked with a message to user.
47
- * See SPEC.md §8.2
48
- */
49
- export class BlockRequest extends Error {
50
- constructor(message: string) {
51
- super(message);
52
- this.name = "BlockRequest";
53
- // Maintains proper prototype chain when transpiled to ES5
54
- Object.setPrototypeOf(this, BlockRequest.prototype);
55
- }
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Context prefix resolution
60
- // ---------------------------------------------------------------------------
61
-
62
- /**
63
- * Resolve a context ID query to an index (1-based) using tiered matching.
64
- * Match priority: exact > prefix > substring (all case-insensitive).
65
- * Returns [index, null] on unique match, [null, error] on 0 or 2+ matches.
66
- * See SPEC.md §8.3
67
- */
68
- export function resolveContextByPrefix(
69
- query: string,
70
- contexts: ContextState[],
71
- ): [number | null, string | null] {
72
- const q = query.toLowerCase();
73
- const available = contexts.map(c => c.id).join(", ");
74
-
75
- // Tier 1: Exact match
76
- const exact = contexts
77
- .map((ctx, i) => [i + 1, ctx] as const)
78
- .filter(([, ctx]) => ctx.id.toLowerCase() === q);
79
- if (exact.length === 1) return [exact[0]![0], null];
80
-
81
- // Tier 2: Prefix match
82
- const prefix = contexts
83
- .map((ctx, i) => [i + 1, ctx] as const)
84
- .filter(([, ctx]) => ctx.id.toLowerCase().startsWith(q));
85
- if (prefix.length === 1) return [prefix[0]![0], null];
86
- if (prefix.length > 1) {
87
- return [null, `Ambiguous match '${query}' — ${prefix.length} prefix matches: ${prefix.map(([, c]) => c.id).join(", ")}. Be more specific.`];
88
- }
89
-
90
- // Tier 3: Substring match
91
- const substr = contexts
92
- .map((ctx, i) => [i + 1, ctx] as const)
93
- .filter(([, ctx]) => ctx.id.toLowerCase().includes(q));
94
- if (substr.length === 1) return [substr[0]![0], null];
95
- if (substr.length > 1) {
96
- return [null, `Ambiguous match '${query}' — ${substr.length} substring matches: ${substr.map(([, c]) => c.id).join(", ")}. Be more specific.`];
97
- }
98
-
99
- return [null, `No context matches '${query}'. Available: ${available}`];
100
- }
101
-
102
- // ---------------------------------------------------------------------------
103
- // Caret command parsing
104
- // ---------------------------------------------------------------------------
105
-
106
- /**
107
- * Parse chained caret commands from user prompt.
108
- * See SPEC.md §8.4
109
- */
110
- export function parseChainedCaret(
111
- prompt: string,
112
- contexts: ContextState[],
113
- ): [CaretCommand | null, string | null] {
114
- if (!prompt.startsWith("^")) return [null, null];
115
-
116
- const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
117
- if (!match) {
118
- return [null, "Invalid prefix. Use ^E<N> to end, ^S<N> to select, or ^0 <desc> for new context."];
119
- }
120
-
121
- const commandStr = match[1]!;
122
- const remaining = (match[2] ?? "").trim();
123
-
124
- // ^N shorthand
125
- if (/^\d+$/.test(commandStr)) {
126
- const num = parseInt(commandStr, 10);
127
- if (num === 0) {
128
- if (remaining.length < MIN_NEW_CONTEXT_CHARS) {
129
- return [null,
130
- `Please provide a longer description for your new context.\n` +
131
- `Your description '${remaining}' is only ${remaining.length} characters.\n` +
132
- `Minimum required: ${MIN_NEW_CONTEXT_CHARS} characters.\n` +
133
- `Example: ^0 implement user authentication with JWT tokens`,
134
- ];
135
- }
136
- return [{ ends: [], select: null, new_context_desc: remaining, remaining_prompt: "" }, null];
137
- }
138
- if (num < 1 || num > contexts.length) {
139
- if (contexts.length === 0) {
140
- return [null, "No existing contexts. Use ^0 <description> to create a new one."];
141
- }
142
- return [null, `Invalid selection. Choose 1-${contexts.length} for existing contexts, or ^0 for new.`];
143
- }
144
- const ctx = contexts[num - 1]!;
145
- return [{ ends: [], select: ctx.id, new_context_desc: null, remaining_prompt: remaining }, null];
146
- }
147
-
148
- // Parse chained commands
149
- const ends: string[] = [];
150
- let select: string | null = null;
151
- let pos = 0;
152
-
153
- while (pos < commandStr.length) {
154
- const ch = commandStr[pos]!.toUpperCase();
155
-
156
- if (ch === "E") {
157
- pos++;
158
- if (pos < commandStr.length && commandStr[pos] === "*") {
159
- pos++;
160
- if (contexts.length === 0) return [null, "No contexts to end."];
161
- for (const ctx of contexts) {
162
- if (!ends.includes(ctx.id)) ends.push(ctx.id);
163
- }
164
- } else if (pos < commandStr.length && commandStr[pos] === ":") {
165
- pos++;
166
- const prefixStart = pos;
167
- while (pos < commandStr.length && !/[EeSs]/.test(commandStr[pos]!)) pos++;
168
- const pfx = commandStr.slice(prefixStart, pos);
169
- if (!pfx) return [null, "Expected ID query after 'E:'"];
170
- const [idx, err] = resolveContextByPrefix(pfx, contexts);
171
- if (err) return [null, err];
172
- const ctx = contexts[idx! - 1]!;
173
- if (!ends.includes(ctx.id)) ends.push(ctx.id);
174
- } else {
175
- const numStart = pos;
176
- while (pos < commandStr.length && /\d/.test(commandStr[pos]!)) pos++;
177
- if (numStart === pos) {
178
- return [null, `Expected number, '*', or ':prefix' after 'E' at position ${numStart + 1}`];
179
- }
180
- const num = parseInt(commandStr.slice(numStart, pos), 10);
181
- if (num < 1 || num > contexts.length) {
182
- if (contexts.length === 0) return [null, "No contexts to end."];
183
- return [null, `Context ^E${num} invalid. Choose 1-${contexts.length}.`];
184
- }
185
- if (pos < commandStr.length && commandStr[pos] === "+") {
186
- pos++;
187
- for (let i = num; i <= contexts.length; i++) {
188
- const ctx = contexts[i - 1]!;
189
- if (!ends.includes(ctx.id)) ends.push(ctx.id);
190
- }
191
- } else {
192
- const ctx = contexts[num - 1]!;
193
- if (!ends.includes(ctx.id)) ends.push(ctx.id);
194
- }
195
- }
196
- } else if (ch === "S") {
197
- pos++;
198
- let ctx: ContextState;
199
- if (pos < commandStr.length && commandStr[pos] === ":") {
200
- pos++;
201
- const prefixStart = pos;
202
- while (pos < commandStr.length && !/[EeSs]/.test(commandStr[pos]!)) pos++;
203
- const pfx = commandStr.slice(prefixStart, pos);
204
- if (!pfx) return [null, "Expected ID query after 'S:'"];
205
- const [idx, err] = resolveContextByPrefix(pfx, contexts);
206
- if (err) return [null, err];
207
- ctx = contexts[idx! - 1]!;
208
- } else {
209
- const numStart = pos;
210
- while (pos < commandStr.length && /\d/.test(commandStr[pos]!)) pos++;
211
- if (numStart === pos) {
212
- return [null, `Expected number or ':prefix' after 'S' at position ${numStart + 1}`];
213
- }
214
- const num = parseInt(commandStr.slice(numStart, pos), 10);
215
- if (num < 1 || num > contexts.length) {
216
- if (contexts.length === 0) return [null, "No contexts to select."];
217
- return [null, `Context ^S${num} invalid. Choose 1-${contexts.length}.`];
218
- }
219
- ctx = contexts[num - 1]!;
220
- }
221
- if (select === null) select = ctx.id;
222
- } else {
223
- return [null,
224
- `Invalid command '${commandStr[pos]}' at position ${pos + 1}.\n` +
225
- `Use E<N> to end, E<N>+ to end N and after, E* to end all, S<N> to select.\n` +
226
- `Example: ^E1S2 (end 1, select 2), ^E2+ (end 2 and older), ^E* (end all)`,
227
- ];
228
- }
229
- }
230
-
231
- if (select !== null && ends.includes(select)) {
232
- return [null, `Cannot select context '${select}' because it's being ended.`];
233
- }
234
-
235
- return [{ ends, select, new_context_desc: null, remaining_prompt: remaining }, null];
236
- }
237
-
238
- // ---------------------------------------------------------------------------
239
- // Plan content matching (fallback)
240
- // ---------------------------------------------------------------------------
241
-
242
- function matchPlanContent(prompt: string, hasPlanContexts: ContextState[]): ContextState | null {
243
- if (hasPlanContexts.length === 0) return null;
244
-
245
- // Tier 1: Plan ID match
246
- const idMatch = prompt.match(/<!-- plan-id: ([a-f0-9]+) -->/);
247
- if (idMatch) {
248
- const foundId = idMatch[1]!;
249
- for (const ctx of hasPlanContexts) {
250
- if (ctx.plan_id === foundId) {
251
- logDebug("context_selector", `Tier 1 plan-id match: ${ctx.id} (id: ${foundId})`);
252
- return ctx;
253
- }
254
- }
255
- }
256
-
257
- // Tier 2: Normalized hash match
258
- const normalized = normalizePlanContent(prompt);
259
- const normHash = crypto.createHash("sha256").update(normalized, "utf-8").digest("hex").slice(0, 12);
260
- for (const ctx of hasPlanContexts) {
261
- if (ctx.plan_hash && ctx.plan_hash === normHash) {
262
- logDebug("context_selector", `Tier 2 normalized hash match: ${ctx.id} (hash: ${normHash})`);
263
- return ctx;
264
- }
265
- }
266
-
267
- // Tier 3: Multi-anchor signature match
268
- for (const ctx of hasPlanContexts) {
269
- const anchors = ctx.plan_anchors ?? [];
270
- if (anchors.length > 0) {
271
- const hits = anchors.filter(a => prompt.includes(a)).length;
272
- if (hits >= 2 && hits >= Math.floor(anchors.length / 2)) {
273
- logDebug("context_selector", `Tier 3 anchor match: ${ctx.id} (${hits}/${anchors.length} anchors)`);
274
- return ctx;
275
- }
276
- }
277
- }
278
-
279
- // Tier 4 (legacy): Signature match
280
- const promptHead = prompt.slice(0, 500);
281
- for (const ctx of hasPlanContexts) {
282
- if (ctx.plan_signature && promptHead.includes(ctx.plan_signature)) {
283
- logDebug("context_selector", `Tier 4 legacy signature match: ${ctx.id}`);
284
- return ctx;
285
- }
286
- }
287
-
288
- return null;
289
- }
290
-
291
- // ---------------------------------------------------------------------------
292
- // Context creation helper
293
- // ---------------------------------------------------------------------------
294
-
295
- function createNewContext(
296
- prompt: string,
297
- projectRoot?: string,
298
- ): [string | null, string, string | null] {
299
- try {
300
- const newCtx = createContextFromPrompt(prompt, projectRoot);
301
- updateMode(newCtx.id, "active", projectRoot);
302
- newCtx.mode = "active";
303
- logInfo("context_selector", `Auto-created context: ${newCtx.id}`);
304
- return [newCtx.id, "auto_created", formatContextCreated(newCtx)];
305
- } catch (e: any) {
306
- logError("context_selector", `Primary context creation failed: ${e}`);
307
- try {
308
- const now = new Date();
309
- const yy = String(now.getFullYear()).slice(2);
310
- const mm = String(now.getMonth() + 1).padStart(2, "0");
311
- const dd = String(now.getDate()).padStart(2, "0");
312
- const hh = String(now.getHours()).padStart(2, "0");
313
- const min = String(now.getMinutes()).padStart(2, "0");
314
- const fallbackId = `${yy}${mm}${dd}-${hh}${min}-context`;
315
- const newCtx = createContext(
316
- fallbackId,
317
- prompt.trim().slice(0, 200) || "New context",
318
- "auto-created-fallback",
319
- projectRoot,
320
- ["auto-created", "fallback"],
321
- );
322
- updateMode(newCtx.id, "active", projectRoot);
323
- newCtx.mode = "active";
324
- logInfo("context_selector", `Fallback context created: ${newCtx.id}`);
325
- return [newCtx.id, "auto_created_fallback", formatContextCreated(newCtx)];
326
- } catch (e2: any) {
327
- logError("context_selector", `ALL context creation failed: ${e2}`);
328
- return [null, "creation_failed", null];
329
- }
330
- }
331
- }
332
-
333
- // ---------------------------------------------------------------------------
334
- // Caret command handler
335
- // ---------------------------------------------------------------------------
336
-
337
- function handleCaretCommand(
338
- prompt: string,
339
- contexts: ContextState[],
340
- projectRoot?: string,
341
- ): [string | null, string, string | null] {
342
- if (contexts.length === 0) {
343
- const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
344
- if (!match) {
345
- throw new BlockRequest(
346
- "Invalid prefix. Use ^0 <description> to create a new context.\n" +
347
- "Example: ^0 implement user authentication system",
348
- );
349
- }
350
- const prefixValue = match[1]!;
351
- const remaining = match[2] ?? "";
352
- if (!/^\d+$/.test(prefixValue) || parseInt(prefixValue, 10) !== 0) {
353
- throw new BlockRequest(
354
- "No existing contexts to select. Use ^0 <description> to create a new context.\n" +
355
- "Example: ^0 implement user authentication system",
356
- );
357
- }
358
- const description = remaining.trim();
359
- if (description.length < MIN_NEW_CONTEXT_CHARS) {
360
- throw new BlockRequest(
361
- `Please provide a longer description for your new context.\n` +
362
- `Your description '${description}' is only ${description.length} characters.\n` +
363
- `Minimum required: ${MIN_NEW_CONTEXT_CHARS} characters.\n` +
364
- `Example: ^0 implement user authentication with JWT tokens`,
365
- );
366
- }
367
- return createNewContext(description, projectRoot);
368
- }
369
-
370
- const [cmd, error] = parseChainedCaret(prompt, contexts);
371
- if (error) throw new BlockRequest(error + "\n" + formatContextPickerStderr(contexts));
372
- if (!cmd) throw new BlockRequest(formatContextPickerStderr(contexts));
373
-
374
- const endedContexts: ContextState[] = [];
375
- for (const ctxId of cmd.ends) {
376
- const ctxToEnd = contexts.find(c => c.id === ctxId);
377
- if (!ctxToEnd) {
378
- throw new BlockRequest(`Context '${ctxId}' no longer exists.\n` + formatContextPickerStderr(contexts));
379
- }
380
- completeContext(ctxToEnd.id, projectRoot);
381
- endedContexts.push(ctxToEnd);
382
- logInfo("context_selector", `Ended context: ${ctxToEnd.id}`);
383
- }
384
-
385
- if (cmd.new_context_desc) {
386
- const [ctxId, method, output] = createNewContext(cmd.new_context_desc, projectRoot);
387
- if (ctxId && endedContexts.length > 0) {
388
- const newCtx = getContext(ctxId, projectRoot);
389
- const feedback = formatCommandFeedback(endedContexts, newCtx);
390
- return [ctxId, method !== "creation_failed" ? "caret_new" : method, feedback];
391
- }
392
- return [ctxId, method !== "creation_failed" ? "caret_new" : method, output];
393
- }
394
-
395
- if (cmd.select) {
396
- const selectedCtx = contexts.find(c => c.id === cmd.select);
397
- if (!selectedCtx) {
398
- throw new BlockRequest(`Context '${cmd.select}' no longer exists.\n` + formatContextPickerStderr(contexts));
399
- }
400
- logInfo("context_selector", `Caret-selected context: ${selectedCtx.id}`);
401
- return [selectedCtx.id, "caret_select", formatCommandFeedback(endedContexts, selectedCtx)];
402
- }
403
-
404
- if (endedContexts.length > 0) {
405
- const remainingContexts = getAllContexts("active", projectRoot);
406
- const feedback = formatCommandFeedback(endedContexts, null);
407
- if (remainingContexts.length === 0) {
408
- throw new BlockRequest(
409
- feedback + "\nAll contexts have been ended. No context selected.\n\n" +
410
- "Just type your task to start a new context.\n" +
411
- "Example: implement user authentication system",
412
- );
413
- }
414
- throw new BlockRequest(
415
- feedback + "\nNo context selected.\n\nSelect a context to continue:\n" +
416
- formatContextPickerStderr(remainingContexts),
417
- );
418
- }
419
-
420
- throw new BlockRequest(formatContextPickerStderr(contexts));
421
- }
422
-
423
- // ---------------------------------------------------------------------------
424
- // Main entry point
425
- // ---------------------------------------------------------------------------
426
-
427
- /**
428
- * Determine which context this prompt belongs to.
429
- * See SPEC.md §8.5
430
- *
431
- * Returns [contextId, method, outputText].
432
- * Throws BlockRequest when request should be blocked to show picker.
433
- */
434
- export function determineContext(
435
- prompt: string,
436
- sessionId?: string,
437
- projectRoot?: string,
438
- ): [string | null, string, string | null] {
439
- if (isInternalCall()) {
440
- logDebug("context_selector", "Skipping: internal subprocess call");
441
- return [null, "skip_internal", null];
442
- }
443
-
444
- // --- Case 1: session_match ---
445
- if (sessionId) {
446
- const sessionContext = getContextBySessionId(sessionId, projectRoot);
447
- if (sessionContext) {
448
- logInfo("context_selector", `Session match: ${sessionContext.id}`);
449
- return [
450
- sessionContext.id,
451
- "session_match",
452
- formatActiveContextReminder(sessionContext, projectRoot),
453
- ];
454
- }
455
- }
456
-
457
- // --- Case 2: caret_command ---
458
- if (prompt.trim() === "^") {
459
- const contexts = getAllContexts("active", projectRoot);
460
- if (contexts.length === 0) {
461
- throw new BlockRequest(
462
- "No contexts exist.\n\nJust type your task to start a new context.\n" +
463
- "Example: implement user authentication system",
464
- );
465
- }
466
- throw new BlockRequest(formatContextPickerStderr(contexts));
467
- }
468
-
469
- if (prompt.startsWith("^")) {
470
- const contexts = getAllContexts("active", projectRoot);
471
- return handleCaretCommand(prompt, contexts, projectRoot);
472
- }
473
-
474
- // --- Case 3: Staged work match (CHANGED: unified mode) ---
475
- const stagedContexts = getAllContexts("active", projectRoot).filter(
476
- (c) => c.mode === "has_staged_work",
477
- );
478
-
479
- if (stagedContexts.length > 0) {
480
- // Separate by artifact type
481
- const planContexts = stagedContexts.filter(
482
- (c) => determineArtifactType(c) === "plan",
483
- );
484
- const handoffContexts = stagedContexts.filter(
485
- (c) => determineArtifactType(c) === "handoff",
486
- );
487
-
488
- // Try plan matching first (content-based matching)
489
- if (planContexts.length > 0) {
490
- const matched = matchPlanContent(prompt, planContexts);
491
- if (matched) {
492
- if (sessionId) bindSession(matched.id, sessionId, projectRoot);
493
- updateMode(matched.id, "active", projectRoot, {
494
- work_consumed: true, // CHANGED: unified flag
495
- plan_hash_consumed: matched.plan_hash,
496
- });
497
- matched.mode = "active";
498
- logInfo("context_selector", `Plan match (fallback): ${matched.id}`);
499
- return [
500
- matched.id,
501
- "plan_content_match",
502
- formatPlanContinuation(matched, projectRoot),
503
- ];
504
- }
505
- }
506
-
507
- // Fallback to handoff (pick first - no content matching)
508
- if (handoffContexts.length > 0) {
509
- const target = handoffContexts[0]!;
510
- if (sessionId) bindSession(target.id, sessionId, projectRoot);
511
- updateMode(target.id, "active", projectRoot, { work_consumed: true }); // CHANGED
512
- target.mode = "active";
513
- logInfo("context_selector", `Handoff match (fallback): ${target.id}`);
514
- return [
515
- target.id,
516
- "handoff_match",
517
- formatHandoffContinuation(target, projectRoot),
518
- ];
519
- }
520
- }
521
-
522
- // --- Case 4: default ---
523
- return createNewContext(prompt, projectRoot);
524
- }
1
+ /**
2
+ * Context selection — determines which context a prompt belongs to.
3
+ * See SPEC.md §8
4
+ *
5
+ * Single entry point: determineContext(prompt, sessionId, projectRoot)
6
+ * Returns [contextId, method, outputText].
7
+ *
8
+ * Selection priority:
9
+ * 1. session_match — session_id found in index.json sessions map
10
+ * 2. caret_command — prompt starts with ^ → parse and execute
11
+ * 3. plan_content_match — FALLBACK: match against has_plan contexts
12
+ * 3b. handoff_match — FALLBACK: match against has_handoff contexts
13
+ * 4. default — create new context
14
+ */
15
+
16
+ import * as crypto from "node:crypto";
17
+ import {
18
+ getContext,
19
+ getAllContexts,
20
+ getContextBySessionId,
21
+ createContextFromPrompt,
22
+ createContext,
23
+ completeContext,
24
+ bindSession,
25
+ updateMode,
26
+ determineArtifactType,
27
+ } from "./context-store.js";
28
+ import {
29
+ formatActiveContextReminder,
30
+ formatContextCreated,
31
+ formatContextPickerStderr,
32
+ formatCommandFeedback,
33
+ formatHandoffContinuation,
34
+ formatPlanContinuation,
35
+ formatActiveContinuation,
36
+ } from "./context-formatter.js";
37
+ import { normalizePlanContent } from "./plan-manager.js";
38
+ import { isInternalCall } from "../base/subprocess-utils.js";
39
+ import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
40
+ import type { ContextState, CaretCommand } from "../types.js";
41
+
42
+ /** Minimum characters required for new context description. */
43
+ const MIN_NEW_CONTEXT_CHARS = 10;
44
+
45
+ /**
46
+ * Raised when the request should be blocked with a message to user.
47
+ * See SPEC.md §8.2
48
+ */
49
+ export class BlockRequest extends Error {
50
+ constructor(message: string) {
51
+ super(message);
52
+ this.name = "BlockRequest";
53
+ // Maintains proper prototype chain when transpiled to ES5
54
+ Object.setPrototypeOf(this, BlockRequest.prototype);
55
+ }
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Context prefix resolution
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * Resolve a context ID query to an index (1-based) using tiered matching.
64
+ * Match priority: exact > prefix > substring (all case-insensitive).
65
+ * Returns [index, null] on unique match, [null, error] on 0 or 2+ matches.
66
+ * See SPEC.md §8.3
67
+ */
68
+ export function resolveContextByPrefix(
69
+ query: string,
70
+ contexts: ContextState[],
71
+ ): [number | null, string | null] {
72
+ const q = query.toLowerCase();
73
+ const available = contexts.map(c => c.id).join(", ");
74
+
75
+ // Tier 1: Exact match
76
+ const exact = contexts
77
+ .map((ctx, i) => [i + 1, ctx] as const)
78
+ .filter(([, ctx]) => ctx.id.toLowerCase() === q);
79
+ if (exact.length === 1) return [exact[0]![0], null];
80
+
81
+ // Tier 2: Prefix match
82
+ const prefix = contexts
83
+ .map((ctx, i) => [i + 1, ctx] as const)
84
+ .filter(([, ctx]) => ctx.id.toLowerCase().startsWith(q));
85
+ if (prefix.length === 1) return [prefix[0]![0], null];
86
+ if (prefix.length > 1) {
87
+ return [null, `Ambiguous match '${query}' — ${prefix.length} prefix matches: ${prefix.map(([, c]) => c.id).join(", ")}. Be more specific.`];
88
+ }
89
+
90
+ // Tier 3: Substring match
91
+ const substr = contexts
92
+ .map((ctx, i) => [i + 1, ctx] as const)
93
+ .filter(([, ctx]) => ctx.id.toLowerCase().includes(q));
94
+ if (substr.length === 1) return [substr[0]![0], null];
95
+ if (substr.length > 1) {
96
+ return [null, `Ambiguous match '${query}' — ${substr.length} substring matches: ${substr.map(([, c]) => c.id).join(", ")}. Be more specific.`];
97
+ }
98
+
99
+ return [null, `No context matches '${query}'. Available: ${available}`];
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Caret command parsing
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Parse chained caret commands from user prompt.
108
+ * See SPEC.md §8.4
109
+ */
110
+ export function parseChainedCaret(
111
+ prompt: string,
112
+ contexts: ContextState[],
113
+ ): [CaretCommand | null, string | null] {
114
+ if (!prompt.startsWith("^")) return [null, null];
115
+
116
+ const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
117
+ if (!match) {
118
+ return [null, "Invalid prefix. Use ^E<N> to end, ^S<N> to select, or ^0 <desc> for new context."];
119
+ }
120
+
121
+ const commandStr = match[1]!;
122
+ const remaining = (match[2] ?? "").trim();
123
+
124
+ // ^N shorthand
125
+ if (/^\d+$/.test(commandStr)) {
126
+ const num = parseInt(commandStr, 10);
127
+ if (num === 0) {
128
+ if (remaining.length < MIN_NEW_CONTEXT_CHARS) {
129
+ return [null,
130
+ `Please provide a longer description for your new context.\n` +
131
+ `Your description '${remaining}' is only ${remaining.length} characters.\n` +
132
+ `Minimum required: ${MIN_NEW_CONTEXT_CHARS} characters.\n` +
133
+ `Example: ^0 implement user authentication with JWT tokens`,
134
+ ];
135
+ }
136
+ return [{ ends: [], select: null, new_context_desc: remaining, remaining_prompt: "" }, null];
137
+ }
138
+ if (num < 1 || num > contexts.length) {
139
+ if (contexts.length === 0) {
140
+ return [null, "No existing contexts. Use ^0 <description> to create a new one."];
141
+ }
142
+ return [null, `Invalid selection. Choose 1-${contexts.length} for existing contexts, or ^0 for new.`];
143
+ }
144
+ const ctx = contexts[num - 1]!;
145
+ return [{ ends: [], select: ctx.id, new_context_desc: null, remaining_prompt: remaining }, null];
146
+ }
147
+
148
+ // Parse chained commands
149
+ const ends: string[] = [];
150
+ let select: string | null = null;
151
+ let pos = 0;
152
+
153
+ while (pos < commandStr.length) {
154
+ const ch = commandStr[pos]!.toUpperCase();
155
+
156
+ if (ch === "E") {
157
+ pos++;
158
+ if (pos < commandStr.length && commandStr[pos] === "*") {
159
+ pos++;
160
+ if (contexts.length === 0) return [null, "No contexts to end."];
161
+ for (const ctx of contexts) {
162
+ if (!ends.includes(ctx.id)) ends.push(ctx.id);
163
+ }
164
+ } else if (pos < commandStr.length && commandStr[pos] === ":") {
165
+ pos++;
166
+ const prefixStart = pos;
167
+ while (pos < commandStr.length && !/[EeSs]/.test(commandStr[pos]!)) pos++;
168
+ const pfx = commandStr.slice(prefixStart, pos);
169
+ if (!pfx) return [null, "Expected ID query after 'E:'"];
170
+ const [idx, err] = resolveContextByPrefix(pfx, contexts);
171
+ if (err) return [null, err];
172
+ const ctx = contexts[idx! - 1]!;
173
+ if (!ends.includes(ctx.id)) ends.push(ctx.id);
174
+ } else {
175
+ const numStart = pos;
176
+ while (pos < commandStr.length && /\d/.test(commandStr[pos]!)) pos++;
177
+ if (numStart === pos) {
178
+ return [null, `Expected number, '*', or ':prefix' after 'E' at position ${numStart + 1}`];
179
+ }
180
+ const num = parseInt(commandStr.slice(numStart, pos), 10);
181
+ if (num < 1 || num > contexts.length) {
182
+ if (contexts.length === 0) return [null, "No contexts to end."];
183
+ return [null, `Context ^E${num} invalid. Choose 1-${contexts.length}.`];
184
+ }
185
+ if (pos < commandStr.length && commandStr[pos] === "+") {
186
+ pos++;
187
+ for (let i = num; i <= contexts.length; i++) {
188
+ const ctx = contexts[i - 1]!;
189
+ if (!ends.includes(ctx.id)) ends.push(ctx.id);
190
+ }
191
+ } else {
192
+ const ctx = contexts[num - 1]!;
193
+ if (!ends.includes(ctx.id)) ends.push(ctx.id);
194
+ }
195
+ }
196
+ } else if (ch === "S") {
197
+ pos++;
198
+ let ctx: ContextState;
199
+ if (pos < commandStr.length && commandStr[pos] === ":") {
200
+ pos++;
201
+ const prefixStart = pos;
202
+ while (pos < commandStr.length && !/[EeSs]/.test(commandStr[pos]!)) pos++;
203
+ const pfx = commandStr.slice(prefixStart, pos);
204
+ if (!pfx) return [null, "Expected ID query after 'S:'"];
205
+ const [idx, err] = resolveContextByPrefix(pfx, contexts);
206
+ if (err) return [null, err];
207
+ ctx = contexts[idx! - 1]!;
208
+ } else {
209
+ const numStart = pos;
210
+ while (pos < commandStr.length && /\d/.test(commandStr[pos]!)) pos++;
211
+ if (numStart === pos) {
212
+ return [null, `Expected number or ':prefix' after 'S' at position ${numStart + 1}`];
213
+ }
214
+ const num = parseInt(commandStr.slice(numStart, pos), 10);
215
+ if (num < 1 || num > contexts.length) {
216
+ if (contexts.length === 0) return [null, "No contexts to select."];
217
+ return [null, `Context ^S${num} invalid. Choose 1-${contexts.length}.`];
218
+ }
219
+ ctx = contexts[num - 1]!;
220
+ }
221
+ if (select === null) select = ctx.id;
222
+ } else {
223
+ return [null,
224
+ `Invalid command '${commandStr[pos]}' at position ${pos + 1}.\n` +
225
+ `Use E<N> to end, E<N>+ to end N and after, E* to end all, S<N> to select.\n` +
226
+ `Example: ^E1S2 (end 1, select 2), ^E2+ (end 2 and older), ^E* (end all)`,
227
+ ];
228
+ }
229
+ }
230
+
231
+ if (select !== null && ends.includes(select)) {
232
+ return [null, `Cannot select context '${select}' because it's being ended.`];
233
+ }
234
+
235
+ return [{ ends, select, new_context_desc: null, remaining_prompt: remaining }, null];
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Plan content matching (fallback)
240
+ // ---------------------------------------------------------------------------
241
+
242
+ function matchPlanContent(prompt: string, hasPlanContexts: ContextState[]): ContextState | null {
243
+ if (hasPlanContexts.length === 0) return null;
244
+
245
+ // Tier 1: Plan ID match
246
+ const idMatch = prompt.match(/<!-- plan-id: ([a-f0-9]+) -->/);
247
+ if (idMatch) {
248
+ const foundId = idMatch[1]!;
249
+ for (const ctx of hasPlanContexts) {
250
+ if (ctx.plan_id === foundId) {
251
+ logDebug("context_selector", `Tier 1 plan-id match: ${ctx.id} (id: ${foundId})`);
252
+ return ctx;
253
+ }
254
+ }
255
+ }
256
+
257
+ // Tier 2: Normalized hash match
258
+ const normalized = normalizePlanContent(prompt);
259
+ const normHash = crypto.createHash("sha256").update(normalized, "utf-8").digest("hex").slice(0, 12);
260
+ for (const ctx of hasPlanContexts) {
261
+ if (ctx.plan_hash && ctx.plan_hash === normHash) {
262
+ logDebug("context_selector", `Tier 2 normalized hash match: ${ctx.id} (hash: ${normHash})`);
263
+ return ctx;
264
+ }
265
+ }
266
+
267
+ // Tier 3: Multi-anchor signature match
268
+ for (const ctx of hasPlanContexts) {
269
+ const anchors = ctx.plan_anchors ?? [];
270
+ if (anchors.length > 0) {
271
+ const hits = anchors.filter(a => prompt.includes(a)).length;
272
+ if (hits >= 2 && hits >= Math.floor(anchors.length / 2)) {
273
+ logDebug("context_selector", `Tier 3 anchor match: ${ctx.id} (${hits}/${anchors.length} anchors)`);
274
+ return ctx;
275
+ }
276
+ }
277
+ }
278
+
279
+ // Tier 4 (legacy): Signature match
280
+ const promptHead = prompt.slice(0, 500);
281
+ for (const ctx of hasPlanContexts) {
282
+ if (ctx.plan_signature && promptHead.includes(ctx.plan_signature)) {
283
+ logDebug("context_selector", `Tier 4 legacy signature match: ${ctx.id}`);
284
+ return ctx;
285
+ }
286
+ }
287
+
288
+ return null;
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // Context creation helper
293
+ // ---------------------------------------------------------------------------
294
+
295
+ function createNewContext(
296
+ prompt: string,
297
+ projectRoot?: string,
298
+ ): [string | null, string, string | null] {
299
+ try {
300
+ const newCtx = createContextFromPrompt(prompt, projectRoot);
301
+ updateMode(newCtx.id, "active", projectRoot);
302
+ newCtx.mode = "active";
303
+ logInfo("context_selector", `Auto-created context: ${newCtx.id}`);
304
+ return [newCtx.id, "auto_created", formatContextCreated(newCtx)];
305
+ } catch (e: any) {
306
+ logError("context_selector", `Primary context creation failed: ${e}`);
307
+ try {
308
+ const now = new Date();
309
+ const yy = String(now.getFullYear()).slice(2);
310
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
311
+ const dd = String(now.getDate()).padStart(2, "0");
312
+ const hh = String(now.getHours()).padStart(2, "0");
313
+ const min = String(now.getMinutes()).padStart(2, "0");
314
+ const fallbackId = `${yy}${mm}${dd}-${hh}${min}-context`;
315
+ const newCtx = createContext(
316
+ fallbackId,
317
+ prompt.trim().slice(0, 200) || "New context",
318
+ "auto-created-fallback",
319
+ projectRoot,
320
+ ["auto-created", "fallback"],
321
+ );
322
+ updateMode(newCtx.id, "active", projectRoot);
323
+ newCtx.mode = "active";
324
+ logInfo("context_selector", `Fallback context created: ${newCtx.id}`);
325
+ return [newCtx.id, "auto_created_fallback", formatContextCreated(newCtx)];
326
+ } catch (e2: any) {
327
+ logError("context_selector", `ALL context creation failed: ${e2}`);
328
+ return [null, "creation_failed", null];
329
+ }
330
+ }
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Caret command handler
335
+ // ---------------------------------------------------------------------------
336
+
337
+ function handleCaretCommand(
338
+ prompt: string,
339
+ contexts: ContextState[],
340
+ projectRoot?: string,
341
+ ): [string | null, string, string | null] {
342
+ if (contexts.length === 0) {
343
+ const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
344
+ if (!match) {
345
+ throw new BlockRequest(
346
+ "Invalid prefix. Use ^0 <description> to create a new context.\n" +
347
+ "Example: ^0 implement user authentication system",
348
+ );
349
+ }
350
+ const prefixValue = match[1]!;
351
+ const remaining = match[2] ?? "";
352
+ if (!/^\d+$/.test(prefixValue) || parseInt(prefixValue, 10) !== 0) {
353
+ throw new BlockRequest(
354
+ "No existing contexts to select. Use ^0 <description> to create a new context.\n" +
355
+ "Example: ^0 implement user authentication system",
356
+ );
357
+ }
358
+ const description = remaining.trim();
359
+ if (description.length < MIN_NEW_CONTEXT_CHARS) {
360
+ throw new BlockRequest(
361
+ `Please provide a longer description for your new context.\n` +
362
+ `Your description '${description}' is only ${description.length} characters.\n` +
363
+ `Minimum required: ${MIN_NEW_CONTEXT_CHARS} characters.\n` +
364
+ `Example: ^0 implement user authentication with JWT tokens`,
365
+ );
366
+ }
367
+ return createNewContext(description, projectRoot);
368
+ }
369
+
370
+ const [cmd, error] = parseChainedCaret(prompt, contexts);
371
+ if (error) throw new BlockRequest(error + "\n" + formatContextPickerStderr(contexts));
372
+ if (!cmd) throw new BlockRequest(formatContextPickerStderr(contexts));
373
+
374
+ const endedContexts: ContextState[] = [];
375
+ for (const ctxId of cmd.ends) {
376
+ const ctxToEnd = contexts.find(c => c.id === ctxId);
377
+ if (!ctxToEnd) {
378
+ throw new BlockRequest(`Context '${ctxId}' no longer exists.\n` + formatContextPickerStderr(contexts));
379
+ }
380
+ completeContext(ctxToEnd.id, projectRoot);
381
+ endedContexts.push(ctxToEnd);
382
+ logInfo("context_selector", `Ended context: ${ctxToEnd.id}`);
383
+ }
384
+
385
+ if (cmd.new_context_desc) {
386
+ const [ctxId, method, output] = createNewContext(cmd.new_context_desc, projectRoot);
387
+ if (ctxId && endedContexts.length > 0) {
388
+ const newCtx = getContext(ctxId, projectRoot);
389
+ const feedback = formatCommandFeedback(endedContexts, newCtx);
390
+ return [ctxId, method !== "creation_failed" ? "caret_new" : method, feedback];
391
+ }
392
+ return [ctxId, method !== "creation_failed" ? "caret_new" : method, output];
393
+ }
394
+
395
+ if (cmd.select) {
396
+ const selectedCtx = contexts.find(c => c.id === cmd.select);
397
+ if (!selectedCtx) {
398
+ throw new BlockRequest(`Context '${cmd.select}' no longer exists.\n` + formatContextPickerStderr(contexts));
399
+ }
400
+ logInfo("context_selector", `Caret-selected context: ${selectedCtx.id}`);
401
+ return [selectedCtx.id, "caret_select", formatCommandFeedback(endedContexts, selectedCtx)];
402
+ }
403
+
404
+ if (endedContexts.length > 0) {
405
+ const remainingContexts = getAllContexts("active", projectRoot);
406
+ const feedback = formatCommandFeedback(endedContexts, null);
407
+ if (remainingContexts.length === 0) {
408
+ throw new BlockRequest(
409
+ feedback + "\nAll contexts have been ended. No context selected.\n\n" +
410
+ "Just type your task to start a new context.\n" +
411
+ "Example: implement user authentication system",
412
+ );
413
+ }
414
+ throw new BlockRequest(
415
+ feedback + "\nNo context selected.\n\nSelect a context to continue:\n" +
416
+ formatContextPickerStderr(remainingContexts),
417
+ );
418
+ }
419
+
420
+ throw new BlockRequest(formatContextPickerStderr(contexts));
421
+ }
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // Main entry point
425
+ // ---------------------------------------------------------------------------
426
+
427
+ /**
428
+ * Determine which context this prompt belongs to.
429
+ * See SPEC.md §8.5
430
+ *
431
+ * Returns [contextId, method, outputText].
432
+ * Throws BlockRequest when request should be blocked to show picker.
433
+ */
434
+ export function determineContext(
435
+ prompt: string,
436
+ sessionId?: string,
437
+ projectRoot?: string,
438
+ ): [string | null, string, string | null] {
439
+ if (isInternalCall()) {
440
+ logDebug("context_selector", "Skipping: internal subprocess call");
441
+ return [null, "skip_internal", null];
442
+ }
443
+
444
+ // --- Case 1: session_match ---
445
+ if (sessionId) {
446
+ const sessionContext = getContextBySessionId(sessionId, projectRoot);
447
+ if (sessionContext) {
448
+ logInfo("context_selector", `Session match: ${sessionContext.id}`);
449
+ return [
450
+ sessionContext.id,
451
+ "session_match",
452
+ formatActiveContextReminder(sessionContext, projectRoot),
453
+ ];
454
+ }
455
+ }
456
+
457
+ // --- Case 2: caret_command ---
458
+ if (prompt.trim() === "^") {
459
+ const contexts = getAllContexts("active", projectRoot);
460
+ if (contexts.length === 0) {
461
+ throw new BlockRequest(
462
+ "No contexts exist.\n\nJust type your task to start a new context.\n" +
463
+ "Example: implement user authentication system",
464
+ );
465
+ }
466
+ throw new BlockRequest(formatContextPickerStderr(contexts));
467
+ }
468
+
469
+ if (prompt.startsWith("^")) {
470
+ const contexts = getAllContexts("active", projectRoot);
471
+ return handleCaretCommand(prompt, contexts, projectRoot);
472
+ }
473
+
474
+ // --- Case 3: Staged work match (CHANGED: unified mode) ---
475
+ const stagedContexts = getAllContexts("active", projectRoot).filter(
476
+ (c) => c.mode === "has_staged_work",
477
+ );
478
+
479
+ if (stagedContexts.length > 0) {
480
+ // Separate by artifact type
481
+ const planContexts = stagedContexts.filter(
482
+ (c) => determineArtifactType(c) === "plan",
483
+ );
484
+ const handoffContexts = stagedContexts.filter(
485
+ (c) => determineArtifactType(c) === "handoff",
486
+ );
487
+
488
+ // Try plan matching first (content-based matching)
489
+ if (planContexts.length > 0) {
490
+ const matched = matchPlanContent(prompt, planContexts);
491
+ if (matched) {
492
+ if (sessionId) bindSession(matched.id, sessionId, projectRoot);
493
+ updateMode(matched.id, "active", projectRoot, {
494
+ work_consumed: true, // CHANGED: unified flag
495
+ plan_hash_consumed: matched.plan_hash,
496
+ });
497
+ matched.mode = "active";
498
+ logInfo("context_selector", `Plan match (fallback): ${matched.id}`);
499
+ return [
500
+ matched.id,
501
+ "plan_content_match",
502
+ formatPlanContinuation(matched, projectRoot),
503
+ ];
504
+ }
505
+ }
506
+
507
+ // Fallback to handoff (pick first - no content matching)
508
+ if (handoffContexts.length > 0) {
509
+ const target = handoffContexts[0]!;
510
+ if (sessionId) bindSession(target.id, sessionId, projectRoot);
511
+ updateMode(target.id, "active", projectRoot, { work_consumed: true }); // CHANGED
512
+ target.mode = "active";
513
+ logInfo("context_selector", `Handoff match (fallback): ${target.id}`);
514
+ return [
515
+ target.id,
516
+ "handoff_match",
517
+ formatHandoffContinuation(target, projectRoot),
518
+ ];
519
+ }
520
+ }
521
+
522
+ // --- Case 4: default ---
523
+ return createNewContext(prompt, projectRoot);
524
+ }