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.
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +8 -60
- package/dist/templates/_shared/.claude/commands/handoff.md +6 -192
- package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
- package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -0
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +10 -11
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +5 -6
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +7 -7
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +145 -34
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
- package/dist/templates/_shared/{workflows → handoff-system/workflows}/handoff.md +6 -6
- package/dist/templates/_shared/hooks-ts/session_end.ts +58 -45
- package/dist/templates/_shared/hooks-ts/session_start.ts +62 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +2 -2
- package/dist/templates/_shared/lib-ts/base/state-io.ts +76 -4
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +8 -2
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +79 -70
- package/dist/templates/_shared/lib-ts/context/context-store.ts +58 -14
- package/dist/templates/_shared/lib-ts/types.ts +9 -3
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +10 -11
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- /package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +0 -0
- /package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +0 -0
- /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,
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
): [
|
|
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,
|
|
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 =
|
|
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:
|
|
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 =
|
|
280
|
+
const promptHead = prompt.slice(0, 500);
|
|
288
281
|
for (const ctx of hasPlanContexts) {
|
|
289
|
-
if (ctx.plan_signature && promptHead.
|
|
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
|
-
): [
|
|
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 (
|
|
313
|
-
logError("context_selector", `Primary context creation failed: ${
|
|
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 (
|
|
334
|
-
logError("context_selector", `ALL context creation failed: ${
|
|
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
|
-
): [
|
|
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) ||
|
|
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
|
|
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
|
-
): [
|
|
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:
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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 ---
|