aiwcli 0.12.3 → 0.12.6

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 (28) hide show
  1. package/dist/templates/_shared/.claude/commands/handoff-resume.md +8 -60
  2. package/dist/templates/_shared/.claude/commands/handoff.md +6 -192
  3. package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
  4. package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
  5. package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -0
  6. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +10 -11
  7. package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +5 -6
  8. package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +7 -7
  9. package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +145 -34
  10. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
  11. package/dist/templates/_shared/{workflows → handoff-system/workflows}/handoff.md +6 -6
  12. package/dist/templates/_shared/hooks-ts/session_end.ts +58 -45
  13. package/dist/templates/_shared/hooks-ts/session_start.ts +62 -50
  14. package/dist/templates/_shared/lib-ts/base/inference.ts +2 -2
  15. package/dist/templates/_shared/lib-ts/base/state-io.ts +76 -4
  16. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
  17. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +8 -2
  18. package/dist/templates/_shared/lib-ts/context/context-selector.ts +79 -70
  19. package/dist/templates/_shared/lib-ts/context/context-store.ts +58 -14
  20. package/dist/templates/_shared/lib-ts/types.ts +9 -3
  21. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +1 -0
  22. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +1 -0
  23. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +10 -11
  24. package/oclif.manifest.json +1 -1
  25. package/package.json +1 -1
  26. /package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +0 -0
  27. /package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +0 -0
  28. /package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/overview.md +0 -0
@@ -19,6 +19,18 @@ const MODE_MIGRATION: Record<string, Mode> = {
19
19
  implementing: "active",
20
20
  };
21
21
 
22
+ /** Legacy state data structure for migration type safety. */
23
+ interface LegacyStateData {
24
+ mode?: string;
25
+ plan_path?: string | null;
26
+ plan_hash?: string | null;
27
+ handoff_path?: string | null;
28
+ plan_consumed?: boolean;
29
+ handoff_consumed?: boolean;
30
+ work_consumed?: boolean;
31
+ next_artifact_type?: "plan" | "handoff" | null;
32
+ }
33
+
22
34
  /**
23
35
  * Serialize a ContextState for JSON output.
24
36
  * Omits null/undefined keys but keeps false, 0, empty string, and empty arrays.
@@ -33,6 +45,59 @@ export function toDict(state: ContextState): Record<string, unknown> {
33
45
  return result;
34
46
  }
35
47
 
48
+ /**
49
+ * Migrate old consumed flags to unified work_consumed.
50
+ * Runs on every state.json read for transparent backward compatibility.
51
+ * Idempotent: safe to run multiple times.
52
+ */
53
+ function migrateConsumedFlags(data: Record<string, unknown>): void {
54
+ const legacy = data as LegacyStateData;
55
+
56
+ // Skip if already migrated (check both fields and mode)
57
+ const alreadyMigrated =
58
+ typeof legacy.work_consumed === "boolean" &&
59
+ legacy.mode !== "has_plan" &&
60
+ legacy.mode !== "has_handoff";
61
+ if (alreadyMigrated) return;
62
+
63
+ const hasPlan = Boolean(legacy.plan_path && legacy.plan_hash);
64
+ const hasHandoff = Boolean(legacy.handoff_path);
65
+
66
+ // Migrate consumed flag (plan takes precedence if both exist)
67
+ if (hasPlan && typeof legacy.plan_consumed === "boolean") {
68
+ (data as Record<string, unknown>).work_consumed = legacy.plan_consumed;
69
+ } else if (hasHandoff && typeof legacy.handoff_consumed === "boolean") {
70
+ (data as Record<string, unknown>).work_consumed = legacy.handoff_consumed;
71
+ } else {
72
+ (data as Record<string, unknown>).work_consumed = false;
73
+ }
74
+
75
+ // Migrate mode: has_plan/has_handoff → has_staged_work
76
+ if (legacy.mode === "has_plan" || legacy.mode === "has_handoff") {
77
+ const artifactType = legacy.mode === "has_handoff" ? "handoff" : "plan";
78
+ (data as Record<string, unknown>).mode = "has_staged_work";
79
+ (data as Record<string, unknown>).next_artifact_type = artifactType;
80
+ }
81
+
82
+ // Set next_artifact_type based on which artifact exists
83
+ if (!legacy.next_artifact_type) {
84
+ if (hasPlan && hasHandoff) {
85
+ // Both exist - conflict resolution: plan priority during migration
86
+ // (Cannot determine "latest" without timestamps - plan takes precedence)
87
+ (data as Record<string, unknown>).next_artifact_type = "plan";
88
+ (data as Record<string, unknown>).handoff_path = null;
89
+ } else if (hasPlan) {
90
+ (data as Record<string, unknown>).next_artifact_type = "plan";
91
+ } else if (hasHandoff) {
92
+ (data as Record<string, unknown>).next_artifact_type = "handoff";
93
+ }
94
+ }
95
+
96
+ // Delete old flags (clean cut migration)
97
+ delete (data as Record<string, unknown>).plan_consumed;
98
+ delete (data as Record<string, unknown>).handoff_consumed;
99
+ }
100
+
36
101
  /**
37
102
  * Get path to state.json for a context.
38
103
  */
@@ -54,6 +119,7 @@ export function readStateJson(
54
119
  try {
55
120
  const raw = fs.readFileSync(sp, "utf-8");
56
121
  const data = JSON.parse(raw) as Record<string, any>;
122
+ migrateConsumedFlags(data); // Migrate before dictToState
57
123
  return dictToState(data);
58
124
  } catch (e: any) {
59
125
  logWarn("state_io", `Failed to read state.json for '${contextId}': ${e}`);
@@ -82,8 +148,14 @@ export function writeStateJson(
82
148
  * Construct a ContextState from a dict, migrating old mode names.
83
149
  * Only includes fields that are present in the source data (preserves null-stripping).
84
150
  */
85
- export function dictToState(data: Record<string, any>): ContextState {
86
- const rawMode: string = data.mode ?? "idle";
151
+ export function dictToState(data: Record<string, unknown>): ContextState {
152
+ // Validate required fields
153
+ if (typeof data.id !== "string" || !data.id) {
154
+ throw new Error("dictToState: missing or invalid required field 'id'");
155
+ }
156
+
157
+ const rawMode: string =
158
+ typeof data.mode === "string" ? data.mode : "idle";
87
159
  const mode: Mode = (MODE_MIGRATION[rawMode] ?? rawMode) as Mode;
88
160
 
89
161
  const state: any = {
@@ -96,8 +168,7 @@ export function dictToState(data: Record<string, any>): ContextState {
96
168
  last_active: data.last_active ?? "",
97
169
  mode,
98
170
  plan_anchors: data.plan_anchors ?? [],
99
- plan_consumed: data.plan_consumed ?? false,
100
- handoff_consumed: data.handoff_consumed ?? false,
171
+ work_consumed: data.work_consumed ?? false,
101
172
  session_ids: data.session_ids ?? [],
102
173
  tasks: data.tasks ?? [],
103
174
  };
@@ -108,6 +179,7 @@ export function dictToState(data: Record<string, any>): ContextState {
108
179
  if ("plan_signature" in data) state.plan_signature = data.plan_signature;
109
180
  if ("plan_id" in data) state.plan_id = data.plan_id;
110
181
  if ("handoff_path" in data) state.handoff_path = data.handoff_path;
182
+ if ("next_artifact_type" in data) state.next_artifact_type = data.next_artifact_type;
111
183
  if ("last_session" in data) state.last_session = data.last_session;
112
184
 
113
185
  // Migration: plan_hash_consumed (added in multi-plan context fix)
@@ -4,6 +4,54 @@
4
4
  */
5
5
 
6
6
  import { execSync, execFile } from "node:child_process";
7
+ import type { ChildProcess } from "node:child_process";
8
+
9
+ // ─── Child Process Cleanup ─────────────────────────────────────────────────
10
+ //
11
+ // Track all spawned child processes to prevent orphaned Node.js processes.
12
+ //
13
+ // Problem: When parent process exits (Ctrl+C, abnormal termination), child
14
+ // processes spawned via execFile can become orphaned if they haven't completed.
15
+ //
16
+ // Solution: Track children in a Set, register exit/signal handlers to kill all
17
+ // tracked children before parent exits.
18
+ //
19
+ // Windows behavior: On Windows with shell:true, execFile spawns cmd.exe which
20
+ // spawns the actual node.exe. Using SIGKILL ensures both are terminated.
21
+
22
+ const childProcesses = new Set<ChildProcess>();
23
+
24
+ /**
25
+ * Cleanup all tracked child processes.
26
+ * Called by exit and signal handlers.
27
+ */
28
+ function cleanupChildren(): void {
29
+ childProcesses.forEach((child) => {
30
+ try {
31
+ // Use SIGKILL for forceful termination (important on Windows with shell)
32
+ child.kill("SIGKILL");
33
+ } catch {
34
+ // Ignore errors (child may have already exited)
35
+ }
36
+ });
37
+ childProcesses.clear();
38
+ }
39
+
40
+ // Register exit handler (runs when process exits normally)
41
+ process.on("exit", () => {
42
+ cleanupChildren();
43
+ });
44
+
45
+ // Register signal handlers (Ctrl+C, kill command)
46
+ process.on("SIGINT", () => {
47
+ cleanupChildren();
48
+ process.exit(130); // Standard exit code for SIGINT
49
+ });
50
+
51
+ process.on("SIGTERM", () => {
52
+ cleanupChildren();
53
+ process.exit(143); // Standard exit code for SIGTERM
54
+ });
7
55
 
8
56
  /**
9
57
  * Check if this is an internal subprocess call.
@@ -172,6 +220,14 @@ export function execFileAsync(
172
220
  },
173
221
  );
174
222
 
223
+ // Track child process for cleanup on abnormal parent exit
224
+ childProcesses.add(child);
225
+
226
+ // Remove from tracking when child exits (normal completion)
227
+ child.on("exit", () => {
228
+ childProcesses.delete(child);
229
+ });
230
+
175
231
  // Pipe input to stdin if provided
176
232
  if (options?.input != null && child.stdin) {
177
233
  child.stdin.write(options.input);
@@ -21,8 +21,7 @@ const MAX_PLAN_INLINE_CHARS = 30_000;
21
21
 
22
22
  const MODE_DISPLAY_MAP: Record<string, string> = {
23
23
  idle: "",
24
- has_plan: "[Plan Ready]",
25
- has_handoff: "[Handoff Ready]",
24
+ has_staged_work: "[Staged]", // CHANGED: unified mode (plan or handoff)
26
25
  active: "[Active]",
27
26
  };
28
27
 
@@ -449,6 +448,7 @@ const KNOWN_FOLDERS: Record<string, string> = {
449
448
  "session-transcripts": "JSONL records of previous agent sessions — read these to understand prior work",
450
449
  "handoffs": "Structured briefing documents for session continuity",
451
450
  "reviews": "Plan review artifacts (reviewer verdicts, corroboration reports)",
451
+ "notes": "Analysis files, reports, and documentation that don't belong in the codebase",
452
452
  };
453
453
 
454
454
  function collectFolderPath(contextId: string, contextDir: string, state: ContextState): string | null {
@@ -532,12 +532,18 @@ function collectSessionStats(contextId: string, contextDir: string, state: Conte
532
532
  return line;
533
533
  }
534
534
 
535
+ function collectNotesGuidance(contextId: string, contextDir: string, state: ContextState): string | null {
536
+ const notesDir = path.join(contextDir, "notes");
537
+ return `**Notes:** Put notes and files that don't belong in the codebase here. Reference them in other documents as needed: \`${notesDir}\``;
538
+ }
539
+
535
540
  /** Ordered list of inventory collectors. Append new collectors here. */
536
541
  const INVENTORY_COLLECTORS: InventoryCollector[] = [
537
542
  collectFolderPath,
538
543
  collectStatePointers,
539
544
  collectFolderInventory,
540
545
  collectSessionStats,
546
+ collectNotesGuidance,
541
547
  ];
542
548
 
543
549
  /**
@@ -14,30 +14,30 @@
14
14
  */
15
15
 
16
16
  import * as crypto from "node:crypto";
17
-
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";
18
28
  import {
19
29
  formatActiveContextReminder,
20
- formatActiveContinuation as _formatActiveContinuation,
21
- formatCommandFeedback,
22
30
  formatContextCreated,
23
31
  formatContextPickerStderr,
32
+ formatCommandFeedback,
24
33
  formatHandoffContinuation,
25
34
  formatPlanContinuation,
35
+ formatActiveContinuation,
26
36
  } from "./context-formatter.js";
27
- import {
28
- bindSession,
29
- completeContext,
30
- createContext,
31
- createContextFromPrompt,
32
- getAllContexts,
33
- getContext,
34
- getContextBySessionId,
35
- updateMode,
36
- } from "./context-store.js";
37
37
  import { normalizePlanContent } from "./plan-manager.js";
38
- import { logDebug, logError, logInfo, logWarn as _logWarn } from "../base/logger.js";
39
38
  import { isInternalCall } from "../base/subprocess-utils.js";
40
- import type { CaretCommand, ContextState } from "../types.js";
39
+ import { logDebug, logInfo, logWarn, logError } from "../base/logger.js";
40
+ import type { ContextState, CaretCommand } from "../types.js";
41
41
 
42
42
  /** Minimum characters required for new context description. */
43
43
  const MIN_NEW_CONTEXT_CHARS = 10;
@@ -50,6 +50,8 @@ export class BlockRequest extends Error {
50
50
  constructor(message: string) {
51
51
  super(message);
52
52
  this.name = "BlockRequest";
53
+ // Maintains proper prototype chain when transpiled to ES5
54
+ Object.setPrototypeOf(this, BlockRequest.prototype);
53
55
  }
54
56
  }
55
57
 
@@ -66,7 +68,7 @@ export class BlockRequest extends Error {
66
68
  export function resolveContextByPrefix(
67
69
  query: string,
68
70
  contexts: ContextState[],
69
- ): [null | number, null | string] {
71
+ ): [number | null, string | null] {
70
72
  const q = query.toLowerCase();
71
73
  const available = contexts.map(c => c.id).join(", ");
72
74
 
@@ -108,7 +110,7 @@ export function resolveContextByPrefix(
108
110
  export function parseChainedCaret(
109
111
  prompt: string,
110
112
  contexts: ContextState[],
111
- ): [CaretCommand | null, null | string] {
113
+ ): [CaretCommand | null, string | null] {
112
114
  if (!prompt.startsWith("^")) return [null, null];
113
115
 
114
116
  const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
@@ -121,7 +123,7 @@ export function parseChainedCaret(
121
123
 
122
124
  // ^N shorthand
123
125
  if (/^\d+$/.test(commandStr)) {
124
- const num = Number.parseInt(commandStr, 10);
126
+ const num = parseInt(commandStr, 10);
125
127
  if (num === 0) {
126
128
  if (remaining.length < MIN_NEW_CONTEXT_CHARS) {
127
129
  return [null,
@@ -131,25 +133,21 @@ export function parseChainedCaret(
131
133
  `Example: ^0 implement user authentication with JWT tokens`,
132
134
  ];
133
135
  }
134
-
135
136
  return [{ ends: [], select: null, new_context_desc: remaining, remaining_prompt: "" }, null];
136
137
  }
137
-
138
138
  if (num < 1 || num > contexts.length) {
139
139
  if (contexts.length === 0) {
140
140
  return [null, "No existing contexts. Use ^0 <description> to create a new one."];
141
141
  }
142
-
143
142
  return [null, `Invalid selection. Choose 1-${contexts.length} for existing contexts, or ^0 for new.`];
144
143
  }
145
-
146
144
  const ctx = contexts[num - 1]!;
147
145
  return [{ ends: [], select: ctx.id, new_context_desc: null, remaining_prompt: remaining }, null];
148
146
  }
149
147
 
150
148
  // Parse chained commands
151
149
  const ends: string[] = [];
152
- let select: null | string = null;
150
+ let select: string | null = null;
153
151
  let pos = 0;
154
152
 
155
153
  while (pos < commandStr.length) {
@@ -179,13 +177,11 @@ export function parseChainedCaret(
179
177
  if (numStart === pos) {
180
178
  return [null, `Expected number, '*', or ':prefix' after 'E' at position ${numStart + 1}`];
181
179
  }
182
-
183
- const num = Number.parseInt(commandStr.slice(numStart, pos), 10);
180
+ const num = parseInt(commandStr.slice(numStart, pos), 10);
184
181
  if (num < 1 || num > contexts.length) {
185
182
  if (contexts.length === 0) return [null, "No contexts to end."];
186
183
  return [null, `Context ^E${num} invalid. Choose 1-${contexts.length}.`];
187
184
  }
188
-
189
185
  if (pos < commandStr.length && commandStr[pos] === "+") {
190
186
  pos++;
191
187
  for (let i = num; i <= contexts.length; i++) {
@@ -215,16 +211,13 @@ export function parseChainedCaret(
215
211
  if (numStart === pos) {
216
212
  return [null, `Expected number or ':prefix' after 'S' at position ${numStart + 1}`];
217
213
  }
218
-
219
- const num = Number.parseInt(commandStr.slice(numStart, pos), 10);
214
+ const num = parseInt(commandStr.slice(numStart, pos), 10);
220
215
  if (num < 1 || num > contexts.length) {
221
216
  if (contexts.length === 0) return [null, "No contexts to select."];
222
217
  return [null, `Context ^S${num} invalid. Choose 1-${contexts.length}.`];
223
218
  }
224
-
225
219
  ctx = contexts[num - 1]!;
226
220
  }
227
-
228
221
  if (select === null) select = ctx.id;
229
222
  } else {
230
223
  return [null,
@@ -284,9 +277,9 @@ function matchPlanContent(prompt: string, hasPlanContexts: ContextState[]): Cont
284
277
  }
285
278
 
286
279
  // Tier 4 (legacy): Signature match
287
- const promptHead = new Set(prompt.slice(0, 500));
280
+ const promptHead = prompt.slice(0, 500);
288
281
  for (const ctx of hasPlanContexts) {
289
- if (ctx.plan_signature && promptHead.has(ctx.plan_signature)) {
282
+ if (ctx.plan_signature && promptHead.includes(ctx.plan_signature)) {
290
283
  logDebug("context_selector", `Tier 4 legacy signature match: ${ctx.id}`);
291
284
  return ctx;
292
285
  }
@@ -302,15 +295,15 @@ function matchPlanContent(prompt: string, hasPlanContexts: ContextState[]): Cont
302
295
  function createNewContext(
303
296
  prompt: string,
304
297
  projectRoot?: string,
305
- ): [null | string, string, null | string] {
298
+ ): [string | null, string, string | null] {
306
299
  try {
307
300
  const newCtx = createContextFromPrompt(prompt, projectRoot);
308
301
  updateMode(newCtx.id, "active", projectRoot);
309
302
  newCtx.mode = "active";
310
303
  logInfo("context_selector", `Auto-created context: ${newCtx.id}`);
311
304
  return [newCtx.id, "auto_created", formatContextCreated(newCtx)];
312
- } catch (error: any) {
313
- logError("context_selector", `Primary context creation failed: ${error}`);
305
+ } catch (e: any) {
306
+ logError("context_selector", `Primary context creation failed: ${e}`);
314
307
  try {
315
308
  const now = new Date();
316
309
  const yy = String(now.getFullYear()).slice(2);
@@ -330,8 +323,8 @@ function createNewContext(
330
323
  newCtx.mode = "active";
331
324
  logInfo("context_selector", `Fallback context created: ${newCtx.id}`);
332
325
  return [newCtx.id, "auto_created_fallback", formatContextCreated(newCtx)];
333
- } catch (error: any) {
334
- logError("context_selector", `ALL context creation failed: ${error}`);
326
+ } catch (e2: any) {
327
+ logError("context_selector", `ALL context creation failed: ${e2}`);
335
328
  return [null, "creation_failed", null];
336
329
  }
337
330
  }
@@ -345,7 +338,7 @@ function handleCaretCommand(
345
338
  prompt: string,
346
339
  contexts: ContextState[],
347
340
  projectRoot?: string,
348
- ): [null | string, string, null | string] {
341
+ ): [string | null, string, string | null] {
349
342
  if (contexts.length === 0) {
350
343
  const match = prompt.match(/^\^(\S+)(?:\s+(.*))?$/s);
351
344
  if (!match) {
@@ -354,16 +347,14 @@ function handleCaretCommand(
354
347
  "Example: ^0 implement user authentication system",
355
348
  );
356
349
  }
357
-
358
350
  const prefixValue = match[1]!;
359
351
  const remaining = match[2] ?? "";
360
- if (!/^\d+$/.test(prefixValue) || Number.parseInt(prefixValue, 10) !== 0) {
352
+ if (!/^\d+$/.test(prefixValue) || parseInt(prefixValue, 10) !== 0) {
361
353
  throw new BlockRequest(
362
354
  "No existing contexts to select. Use ^0 <description> to create a new context.\n" +
363
355
  "Example: ^0 implement user authentication system",
364
356
  );
365
357
  }
366
-
367
358
  const description = remaining.trim();
368
359
  if (description.length < MIN_NEW_CONTEXT_CHARS) {
369
360
  throw new BlockRequest(
@@ -373,7 +364,6 @@ function handleCaretCommand(
373
364
  `Example: ^0 implement user authentication with JWT tokens`,
374
365
  );
375
366
  }
376
-
377
367
  return createNewContext(description, projectRoot);
378
368
  }
379
369
 
@@ -387,7 +377,6 @@ function handleCaretCommand(
387
377
  if (!ctxToEnd) {
388
378
  throw new BlockRequest(`Context '${ctxId}' no longer exists.\n` + formatContextPickerStderr(contexts));
389
379
  }
390
-
391
380
  completeContext(ctxToEnd.id, projectRoot);
392
381
  endedContexts.push(ctxToEnd);
393
382
  logInfo("context_selector", `Ended context: ${ctxToEnd.id}`);
@@ -398,10 +387,9 @@ function handleCaretCommand(
398
387
  if (ctxId && endedContexts.length > 0) {
399
388
  const newCtx = getContext(ctxId, projectRoot);
400
389
  const feedback = formatCommandFeedback(endedContexts, newCtx);
401
- return [ctxId, method === "creation_failed" ? method : "caret_new", feedback];
390
+ return [ctxId, method !== "creation_failed" ? "caret_new" : method, feedback];
402
391
  }
403
-
404
- return [ctxId, method === "creation_failed" ? method : "caret_new", output];
392
+ return [ctxId, method !== "creation_failed" ? "caret_new" : method, output];
405
393
  }
406
394
 
407
395
  if (cmd.select) {
@@ -409,7 +397,6 @@ function handleCaretCommand(
409
397
  if (!selectedCtx) {
410
398
  throw new BlockRequest(`Context '${cmd.select}' no longer exists.\n` + formatContextPickerStderr(contexts));
411
399
  }
412
-
413
400
  logInfo("context_selector", `Caret-selected context: ${selectedCtx.id}`);
414
401
  return [selectedCtx.id, "caret_select", formatCommandFeedback(endedContexts, selectedCtx)];
415
402
  }
@@ -424,7 +411,6 @@ function handleCaretCommand(
424
411
  "Example: implement user authentication system",
425
412
  );
426
413
  }
427
-
428
414
  throw new BlockRequest(
429
415
  feedback + "\nNo context selected.\n\nSelect a context to continue:\n" +
430
416
  formatContextPickerStderr(remainingContexts),
@@ -449,7 +435,7 @@ export function determineContext(
449
435
  prompt: string,
450
436
  sessionId?: string,
451
437
  projectRoot?: string,
452
- ): [null | string, string, null | string] {
438
+ ): [string | null, string, string | null] {
453
439
  if (isInternalCall()) {
454
440
  logDebug("context_selector", "Skipping: internal subprocess call");
455
441
  return [null, "skip_internal", null];
@@ -477,7 +463,6 @@ export function determineContext(
477
463
  "Example: implement user authentication system",
478
464
  );
479
465
  }
480
-
481
466
  throw new BlockRequest(formatContextPickerStderr(contexts));
482
467
  }
483
468
 
@@ -486,28 +471,52 @@ export function determineContext(
486
471
  return handleCaretCommand(prompt, contexts, projectRoot);
487
472
  }
488
473
 
489
- // --- Case 3: plan_content_match (fallback) ---
490
- const hasPlanContexts = getAllContexts("active", projectRoot).filter(c => c.mode === "has_plan");
491
- if (hasPlanContexts.length > 0) {
492
- const matched = matchPlanContent(prompt, hasPlanContexts);
493
- if (matched) {
494
- if (sessionId) bindSession(matched.id, sessionId, projectRoot);
495
- updateMode(matched.id, "active", projectRoot, { plan_consumed: true });
496
- matched.mode = "active";
497
- logInfo("context_selector", `Plan match (fallback): ${matched.id}`);
498
- return [matched.id, "plan_content_match", formatPlanContinuation(matched, projectRoot)];
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
+ }
499
505
  }
500
- }
501
506
 
502
- // --- Case 3b: handoff_match (fallback) ---
503
- const hasHandoffContexts = getAllContexts("active", projectRoot).filter(c => c.mode === "has_handoff");
504
- if (hasHandoffContexts.length > 0) {
505
- const target = hasHandoffContexts[0]!;
506
- if (sessionId) bindSession(target.id, sessionId, projectRoot);
507
- updateMode(target.id, "active", projectRoot, { handoff_consumed: true });
508
- target.mode = "active";
509
- logInfo("context_selector", `Handoff match (fallback): ${target.id}`);
510
- return [target.id, "handoff_match", formatHandoffContinuation(target, projectRoot)];
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
+ }
511
520
  }
512
521
 
513
522
  // --- Case 4: default ---