@nseng-ai/branch-context 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +48 -0
- package/src/api/index.ts +43 -0
- package/src/api/prompt-assets.ts +4 -0
- package/src/core/attach.ts +455 -0
- package/src/core/attached-plan.ts +447 -0
- package/src/core/branch-context-creation.ts +495 -0
- package/src/core/branch-memory.ts +147 -0
- package/src/core/cli.ts +98 -0
- package/src/core/constants.ts +19 -0
- package/src/core/context.ts +45 -0
- package/src/core/existing-branch-reuse.ts +230 -0
- package/src/core/index.ts +47 -0
- package/src/core/operations.ts +590 -0
- package/src/core/plan-content-slug.ts +50 -0
- package/src/core/prompt-assets.ts +14 -0
- package/src/core/prompts/branch-context-impl.md +34 -0
- package/src/core/session-artifact.ts +114 -0
- package/src/ns/command.ts +33 -0
- package/src/ns/commands/attach.ts +19 -0
- package/src/ns/commands/check.ts +18 -0
- package/src/ns/commands/delete.ts +18 -0
- package/src/ns/commands/from-plan.ts +21 -0
- package/src/ns/commands/list.ts +17 -0
- package/src/ns/commands/load.ts +19 -0
- package/src/ns/repo-local-ns-extension.ts +32 -0
- package/src/pi/enriched-plan-save.ts +551 -0
- package/src/pi/extension.ts +154 -0
- package/src/pi/from-plan-commands.ts +1243 -0
- package/src/pi/gt/upstack-impl-launch.ts +154 -0
- package/src/pi/host-types.ts +135 -0
- package/src/pi/index.ts +16 -0
- package/src/pi/options.ts +32 -0
- package/src/pi/surfaces.ts +11 -0
- package/src/testing/index.ts +258 -0
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
sendCommandProgressOrNotify,
|
|
5
|
+
registerCommandWithImmediateAck,
|
|
6
|
+
} from "@nseng-ai/pi/commands/ack";
|
|
7
|
+
import { setRuntimeStatus } from "@nseng-ai/pi/runtime/status";
|
|
8
|
+
import {
|
|
9
|
+
formatBranchContextGtUpstackImplFollowUpFlow,
|
|
10
|
+
runBranchContextGtUpstackImplLaunch,
|
|
11
|
+
} from "./gt/upstack-impl-launch.ts";
|
|
12
|
+
import {
|
|
13
|
+
BRANCH_CONTEXT_FROM_PLAN_COMMAND_NAME,
|
|
14
|
+
BRANCH_CONTEXT_UPSTACK_IMPL_FROM_PLAN_COMMAND_NAME,
|
|
15
|
+
IMPL_BRANCH_CONTEXT_COMMAND_NAME,
|
|
16
|
+
IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME,
|
|
17
|
+
formatImplBranchContextCommand,
|
|
18
|
+
} from "./surfaces.ts";
|
|
19
|
+
import {
|
|
20
|
+
BRANCH_CONTEXT_NAMESPACE,
|
|
21
|
+
buildBranchContextOutputMessage,
|
|
22
|
+
buildBranchContextPlanKey,
|
|
23
|
+
buildImplBranchContextPrompt,
|
|
24
|
+
createRealBranchContextContext,
|
|
25
|
+
derivePlanContentSlug,
|
|
26
|
+
deriveTargetBranch,
|
|
27
|
+
formatBranchContextEvidence,
|
|
28
|
+
describeBranchContextGraphiteCreationSteps,
|
|
29
|
+
formatExistingBranchContextReuse,
|
|
30
|
+
formatLoadedAttachedPlanEvidence,
|
|
31
|
+
resolveExistingBranchContextReuse,
|
|
32
|
+
type BranchContextEvidence,
|
|
33
|
+
type BranchContextOutputDetails,
|
|
34
|
+
type BranchCreationMethod,
|
|
35
|
+
type ExistingBranchContextReuse,
|
|
36
|
+
type PlanContentSlugEvidence,
|
|
37
|
+
} from "@nseng-ai/branch-context/api";
|
|
38
|
+
import {
|
|
39
|
+
NoSavedPlanAvailableError,
|
|
40
|
+
type RepoIdentitySource,
|
|
41
|
+
type SelectedSavedPlanFile,
|
|
42
|
+
} from "@nseng-ai/plans/api";
|
|
43
|
+
import {
|
|
44
|
+
formatErrorMessage,
|
|
45
|
+
optionalEntries,
|
|
46
|
+
optionalEntry,
|
|
47
|
+
} from "@nseng-ai/foundation/primitives";
|
|
48
|
+
import {
|
|
49
|
+
resolveBranchContextDefaultCreation,
|
|
50
|
+
resolveBranchContextOperations,
|
|
51
|
+
resolvePlanStoreRootOption,
|
|
52
|
+
} from "./options.ts";
|
|
53
|
+
import type {
|
|
54
|
+
BranchContextExtensionOptions,
|
|
55
|
+
BranchContextOperations,
|
|
56
|
+
CommandContext,
|
|
57
|
+
ExtensionAPI,
|
|
58
|
+
NotifyLevel,
|
|
59
|
+
ReplacedSessionContext,
|
|
60
|
+
} from "./host-types.ts";
|
|
61
|
+
|
|
62
|
+
export const CREATE_BRANCH_CONTEXT_COMMAND_NAME = BRANCH_CONTEXT_FROM_PLAN_COMMAND_NAME;
|
|
63
|
+
export const GT_UPSTACK_IMPL_COMMAND_NAME = BRANCH_CONTEXT_UPSTACK_IMPL_FROM_PLAN_COMMAND_NAME;
|
|
64
|
+
export { IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME };
|
|
65
|
+
const BRANCH_CONTEXT_STATUS_KEY = CREATE_BRANCH_CONTEXT_COMMAND_NAME;
|
|
66
|
+
const GT_UPSTACK_IMPL_STATUS_KEY = GT_UPSTACK_IMPL_COMMAND_NAME;
|
|
67
|
+
const IMPL_BRANCH_CONTEXT_STATUS_KEY = IMPL_BRANCH_CONTEXT_COMMAND_NAME;
|
|
68
|
+
const IMPL_CURRENT_SAVED_PLAN_STATUS_KEY = IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME;
|
|
69
|
+
const GRAPHITE_BRANCH_CREATION_HELP =
|
|
70
|
+
describeBranchContextGraphiteCreationSteps("<current-branch>");
|
|
71
|
+
|
|
72
|
+
export const CREATE_BRANCH_CONTEXT_USAGE = `Usage: /${CREATE_BRANCH_CONTEXT_COMMAND_NAME} [options] [absolute-or-home-plan-file.md]
|
|
73
|
+
|
|
74
|
+
Create a branch context from a saved plan. The branch slug is derived from the plan content by a tiny Pi model, then the plan is attached to the branch in Branch Memory as <content-derived-slug>.md.
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
--dry-run Show the selected plan and target branch without mutating.
|
|
78
|
+
--yes, -y Compatibility no-op; resolved branch contexts create without confirmation.
|
|
79
|
+
--graphite Create with the branch-context Graphite method.
|
|
80
|
+
--plain-git Create with plain Git only; no Graphite tracking.
|
|
81
|
+
--branch <name> Use an explicit target branch name.
|
|
82
|
+
--help, -h Show this help.
|
|
83
|
+
|
|
84
|
+
${GRAPHITE_BRANCH_CREATION_HELP}
|
|
85
|
+
|
|
86
|
+
With no file path, the command prefers the most recent saved plan created in the current session, then falls back to the newest .md file in the current repo/source branch local plan store directory.
|
|
87
|
+
An explicit file path may be absolute or current-user home-relative with ~ or ~/; a leading @ is accepted and stripped, and the normalized result must be absolute with a .md filename.
|
|
88
|
+
The saved-plan filename is only a locator. If the model cannot derive and validate a content slug, the command fails without falling back to the filename.`;
|
|
89
|
+
|
|
90
|
+
export const GT_UPSTACK_IMPL_USAGE = `Usage: /${GT_UPSTACK_IMPL_COMMAND_NAME} [options] [absolute-or-home-plan-file.md]
|
|
91
|
+
|
|
92
|
+
Stack a branch context on the current branch with the branch-context Graphite method, attach the saved plan, check out that branch with exact git checkout <branch>, start a fresh Pi session, and run /${IMPL_BRANCH_CONTEXT_COMMAND_NAME} <attached-key> for the attached plan in that new session.
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
--dry-run Show the selected plan and follow-up flow without mutating.
|
|
96
|
+
--yes, -y Compatibility no-op; resolved branch contexts create without confirmation.
|
|
97
|
+
--graphite Default: create with the branch-context Graphite method.
|
|
98
|
+
--plain-git Escape hatch: create with plain Git only; no Graphite tracking, so the branch will not be part of a stack.
|
|
99
|
+
--branch <name> Use an explicit target branch name.
|
|
100
|
+
--help, -h Show this help.
|
|
101
|
+
|
|
102
|
+
${GRAPHITE_BRANCH_CREATION_HELP}
|
|
103
|
+
|
|
104
|
+
The current branch must be trunk or a Graphite-tracked branch; otherwise this command fails before creating a branch or attaching a plan.
|
|
105
|
+
With no file path, the command prefers the most recent saved plan created in the current session, then falls back to the newest .md file in the current repo/source branch local plan store directory.
|
|
106
|
+
An explicit file path may be absolute or current-user home-relative with ~ or ~/; a leading @ is accepted and stripped, and the normalized result must be absolute with a .md filename.
|
|
107
|
+
|
|
108
|
+
This command intentionally models the manual flow: /${CREATE_BRANCH_CONTEXT_COMMAND_NAME} --graphite, git checkout <branch>, /new, then /${IMPL_BRANCH_CONTEXT_COMMAND_NAME} <attached-key> in the new Pi session.`;
|
|
109
|
+
|
|
110
|
+
export const IMPL_CURRENT_SAVED_PLAN_USAGE = `Usage: /${IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME} [options] [absolute-or-home-plan-file.md]
|
|
111
|
+
|
|
112
|
+
Launch a fresh Pi implementation session on the current branch from a saved plan. This command does not create, check out, or attach a branch context.
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--dry-run Show the selected plan and launch flow without mutating or starting a session.
|
|
116
|
+
--help, -h Show this help.
|
|
117
|
+
|
|
118
|
+
With no file path, the command prefers the most recent saved plan created in the current session, then falls back to the newest .md file in the current repo/source branch local plan store directory.
|
|
119
|
+
An explicit file path may be absolute or current-user home-relative with ~ or ~/; a leading @ is accepted and stripped, and the normalized result must be absolute with a .md filename.
|
|
120
|
+
Branch creation flags such as --branch, --graphite, and --plain-git are intentionally unsupported.`;
|
|
121
|
+
|
|
122
|
+
export interface CreateBranchContextArgs {
|
|
123
|
+
help: boolean;
|
|
124
|
+
dryRun: boolean;
|
|
125
|
+
yes: boolean;
|
|
126
|
+
branchName?: string;
|
|
127
|
+
branchCreation?: BranchCreationMethod;
|
|
128
|
+
filePath?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ImplCurrentSavedPlanArgs {
|
|
132
|
+
help: boolean;
|
|
133
|
+
dryRun: boolean;
|
|
134
|
+
filePath?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ImplCurrentSavedPlanPreviewBase {
|
|
138
|
+
savedPlanFileStem: string;
|
|
139
|
+
filePath: string;
|
|
140
|
+
fileName: string;
|
|
141
|
+
planContent: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface ExplicitImplCurrentSavedPlanPreview extends ImplCurrentSavedPlanPreviewBase {
|
|
145
|
+
mode: "explicit";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface StoredImplCurrentSavedPlanPreview extends ImplCurrentSavedPlanPreviewBase {
|
|
149
|
+
mode: "latest" | "session";
|
|
150
|
+
repoRoot: string;
|
|
151
|
+
repoKey: string;
|
|
152
|
+
repoIdentitySource: RepoIdentitySource;
|
|
153
|
+
sourceBranch: string;
|
|
154
|
+
branchKey: string;
|
|
155
|
+
modifiedTimeMs?: number;
|
|
156
|
+
summary?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type ImplCurrentSavedPlanPreview =
|
|
160
|
+
| ExplicitImplCurrentSavedPlanPreview
|
|
161
|
+
| StoredImplCurrentSavedPlanPreview;
|
|
162
|
+
|
|
163
|
+
interface CreateBranchContextPreviewBase {
|
|
164
|
+
slug: string;
|
|
165
|
+
savedPlanFileStem: string;
|
|
166
|
+
filePath: string;
|
|
167
|
+
fileName: string;
|
|
168
|
+
planKey: string;
|
|
169
|
+
targetBranch: string;
|
|
170
|
+
branchNameForCreation?: string;
|
|
171
|
+
isExplicitTargetBranch: boolean;
|
|
172
|
+
slugEvidence: PlanContentSlugEvidence;
|
|
173
|
+
branchCreation: BranchCreationMethod;
|
|
174
|
+
summary?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface ExplicitCreateBranchContextPreview extends CreateBranchContextPreviewBase {
|
|
178
|
+
mode: "explicit";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface StoredCreateBranchContextPreview extends CreateBranchContextPreviewBase {
|
|
182
|
+
mode: "latest" | "session";
|
|
183
|
+
repoRoot: string;
|
|
184
|
+
repoKey: string;
|
|
185
|
+
repoIdentitySource: RepoIdentitySource;
|
|
186
|
+
sourceBranch: string;
|
|
187
|
+
branchKey: string;
|
|
188
|
+
modifiedTimeMs?: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type CreateBranchContextPreview =
|
|
192
|
+
| ExplicitCreateBranchContextPreview
|
|
193
|
+
| StoredCreateBranchContextPreview;
|
|
194
|
+
|
|
195
|
+
interface BranchContextTargetBranch {
|
|
196
|
+
targetBranch: string;
|
|
197
|
+
branchNameForCreation?: string;
|
|
198
|
+
isExplicitTargetBranch: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface ExplicitSavedPlanEvidence {
|
|
202
|
+
mode: "explicit";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface StoredSavedPlanEvidence {
|
|
206
|
+
mode: "latest" | "session";
|
|
207
|
+
repoRoot: string;
|
|
208
|
+
repoKey: string;
|
|
209
|
+
repoIdentitySource: RepoIdentitySource;
|
|
210
|
+
sourceBranch: string;
|
|
211
|
+
branchKey: string;
|
|
212
|
+
modifiedTimeMs: number;
|
|
213
|
+
summary?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type SelectedSavedPlanEvidence = ExplicitSavedPlanEvidence | StoredSavedPlanEvidence;
|
|
217
|
+
|
|
218
|
+
interface RunCreateBranchContextCommandOptions {
|
|
219
|
+
pi: ExtensionAPI;
|
|
220
|
+
rawArgs: string;
|
|
221
|
+
ctx: CommandContext;
|
|
222
|
+
extensionOptions: BranchContextExtensionOptions;
|
|
223
|
+
usage: string;
|
|
224
|
+
statusKey: string;
|
|
225
|
+
progressMessage: string;
|
|
226
|
+
derivePreviewOptions(
|
|
227
|
+
extensionOptions: BranchContextExtensionOptions,
|
|
228
|
+
): BranchContextExtensionOptions;
|
|
229
|
+
formatDryRunMessage(preview: CreateBranchContextPreview): string;
|
|
230
|
+
onCreated(evidence: BranchContextEvidence): Promise<void> | void;
|
|
231
|
+
handleSelectedPlanError?(args: CreateBranchContextArgs, error: unknown): Promise<boolean>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class CreateBranchContextUsageError extends Error {
|
|
235
|
+
constructor(message: string) {
|
|
236
|
+
super(message);
|
|
237
|
+
this.name = "CreateBranchContextUsageError";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveBranchContextContext(
|
|
242
|
+
pi: ExtensionAPI,
|
|
243
|
+
cwd: string,
|
|
244
|
+
options: BranchContextExtensionOptions,
|
|
245
|
+
) {
|
|
246
|
+
return options.createBranchContextContext?.(pi, cwd) ?? createRealBranchContextContext({ cwd });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function parseCreateBranchContextArgs(rawArgs: string): CreateBranchContextArgs {
|
|
250
|
+
const parsed: CreateBranchContextArgs = { help: false, dryRun: false, yes: false };
|
|
251
|
+
const tokens = tokenizeCommandArgs(rawArgs);
|
|
252
|
+
const positional: string[] = [];
|
|
253
|
+
|
|
254
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
255
|
+
const token = tokens[index];
|
|
256
|
+
if (token === undefined) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token === "--help" || token === "-h") {
|
|
261
|
+
parsed.help = true;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (token === "--dry-run") {
|
|
265
|
+
parsed.dryRun = true;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (token === "--yes" || token === "-y") {
|
|
269
|
+
parsed.yes = true;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (token === "--graphite") {
|
|
273
|
+
setBranchCreation(parsed, "graphite");
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (token === "--plain-git") {
|
|
277
|
+
setBranchCreation(parsed, "plain-git");
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (token === "--branch") {
|
|
281
|
+
const value = tokens[index + 1];
|
|
282
|
+
if (value === undefined || value.startsWith("-")) {
|
|
283
|
+
throw new CreateBranchContextUsageError("Missing value for --branch.");
|
|
284
|
+
}
|
|
285
|
+
parsed.branchName = value;
|
|
286
|
+
index += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (token.startsWith("--branch=")) {
|
|
290
|
+
const value = token.slice("--branch=".length);
|
|
291
|
+
if (value.length === 0) {
|
|
292
|
+
throw new CreateBranchContextUsageError("Missing value for --branch.");
|
|
293
|
+
}
|
|
294
|
+
parsed.branchName = value;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (token.startsWith("-")) {
|
|
298
|
+
throw new CreateBranchContextUsageError(`Unknown flag: ${token}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
positional.push(token);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (positional.length > 1) {
|
|
305
|
+
throw new CreateBranchContextUsageError("Expected at most one plan file path.");
|
|
306
|
+
}
|
|
307
|
+
const filePath = positional[0];
|
|
308
|
+
if (filePath !== undefined) {
|
|
309
|
+
parsed.filePath = filePath;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return parsed;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function parseImplCurrentSavedPlanArgs(rawArgs: string): ImplCurrentSavedPlanArgs {
|
|
316
|
+
const parsed: ImplCurrentSavedPlanArgs = { help: false, dryRun: false };
|
|
317
|
+
const positional: string[] = [];
|
|
318
|
+
|
|
319
|
+
for (const token of tokenizeCommandArgs(rawArgs)) {
|
|
320
|
+
if (token === "--help" || token === "-h") {
|
|
321
|
+
parsed.help = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (token === "--dry-run") {
|
|
325
|
+
parsed.dryRun = true;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (token === "--branch" || token.startsWith("--branch=")) {
|
|
329
|
+
throw new CreateBranchContextUsageError(
|
|
330
|
+
"--branch is not supported; this command implements on the current branch.",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (token === "--graphite" || token === "--plain-git") {
|
|
334
|
+
throw new CreateBranchContextUsageError(
|
|
335
|
+
`${token} is not supported; this command does not create branches.`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (token.startsWith("-")) {
|
|
339
|
+
throw new CreateBranchContextUsageError(`Unknown flag: ${token}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
positional.push(token);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (positional.length > 1) {
|
|
346
|
+
throw new CreateBranchContextUsageError("Expected at most one plan file path.");
|
|
347
|
+
}
|
|
348
|
+
const filePath = positional[0];
|
|
349
|
+
if (filePath !== undefined) {
|
|
350
|
+
parsed.filePath = filePath;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return parsed;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function tokenizeCommandArgs(rawArgs: string): string[] {
|
|
357
|
+
return rawArgs
|
|
358
|
+
.trim()
|
|
359
|
+
.split(/\s+/)
|
|
360
|
+
.filter((token) => token.length > 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function setBranchCreation(
|
|
364
|
+
args: CreateBranchContextArgs,
|
|
365
|
+
branchCreation: BranchCreationMethod,
|
|
366
|
+
): void {
|
|
367
|
+
if (args.branchCreation !== undefined && args.branchCreation !== branchCreation) {
|
|
368
|
+
throw new CreateBranchContextUsageError("Cannot pass both --graphite and --plain-git.");
|
|
369
|
+
}
|
|
370
|
+
args.branchCreation = branchCreation;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function resolveCreateBranchContextPlanFile(
|
|
374
|
+
pi: ExtensionAPI,
|
|
375
|
+
args: CreateBranchContextArgs,
|
|
376
|
+
ctx: CommandContext,
|
|
377
|
+
options: BranchContextExtensionOptions = {},
|
|
378
|
+
): Promise<SelectedSavedPlanFile> {
|
|
379
|
+
return resolveSelectedSavedPlanFile(pi, args, ctx, options);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function deriveCreateBranchContextPreview(
|
|
383
|
+
pi: ExtensionAPI,
|
|
384
|
+
args: CreateBranchContextArgs,
|
|
385
|
+
ctx: CommandContext,
|
|
386
|
+
selected: SelectedSavedPlanFile,
|
|
387
|
+
options: BranchContextExtensionOptions = {},
|
|
388
|
+
): Promise<CreateBranchContextPreview> {
|
|
389
|
+
const selectedFile = selectedSavedPlanFileInfo(selected);
|
|
390
|
+
const slugEvidence = await derivePlanContentSlug(pi, {
|
|
391
|
+
filePath: selectedFile.filePath,
|
|
392
|
+
cwd: ctx.cwd,
|
|
393
|
+
});
|
|
394
|
+
const branchCreation = args.branchCreation ?? resolveBranchContextDefaultCreation(options);
|
|
395
|
+
const target = deriveBranchContextTargetBranch(args, slugEvidence.slug, options);
|
|
396
|
+
const planKey = buildBranchContextPlanKey(slugEvidence.slug);
|
|
397
|
+
const base = {
|
|
398
|
+
slug: slugEvidence.slug,
|
|
399
|
+
savedPlanFileStem: selected.savedPlanFileStem,
|
|
400
|
+
filePath: selectedFile.filePath,
|
|
401
|
+
fileName: selectedFile.fileName,
|
|
402
|
+
planKey,
|
|
403
|
+
targetBranch: target.targetBranch,
|
|
404
|
+
...(target.branchNameForCreation === undefined
|
|
405
|
+
? {}
|
|
406
|
+
: { branchNameForCreation: target.branchNameForCreation }),
|
|
407
|
+
isExplicitTargetBranch: target.isExplicitTargetBranch,
|
|
408
|
+
branchCreation,
|
|
409
|
+
slugEvidence,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return { ...base, ...selectedSavedPlanEvidence(selected) };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function resolveCreateBranchContextPreview(
|
|
416
|
+
pi: ExtensionAPI,
|
|
417
|
+
args: CreateBranchContextArgs,
|
|
418
|
+
ctx: CommandContext,
|
|
419
|
+
options: BranchContextExtensionOptions = {},
|
|
420
|
+
): Promise<CreateBranchContextPreview> {
|
|
421
|
+
const selected = await resolveCreateBranchContextPlanFile(pi, args, ctx, options);
|
|
422
|
+
return deriveCreateBranchContextPreview(pi, args, ctx, selected, options);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function formatCreateBranchContextPreview(preview: CreateBranchContextPreview): string {
|
|
426
|
+
const lines = [
|
|
427
|
+
preview.mode === "explicit"
|
|
428
|
+
? "Explicit saved plan file:"
|
|
429
|
+
: preview.mode === "session"
|
|
430
|
+
? "Saved plan from current session:"
|
|
431
|
+
: "Latest saved plan from local plan store:",
|
|
432
|
+
];
|
|
433
|
+
lines.push(`Path: ${preview.filePath}`);
|
|
434
|
+
lines.push(`Saved-plan file stem: ${preview.savedPlanFileStem}`);
|
|
435
|
+
lines.push(`Content-derived slug: ${preview.slug}`);
|
|
436
|
+
lines.push(`Slug model: ${preview.slugEvidence.provider}/${preview.slugEvidence.model}`);
|
|
437
|
+
if (preview.mode !== "explicit") {
|
|
438
|
+
lines.push(`Repo key: ${preview.repoKey}`);
|
|
439
|
+
lines.push(`Repo root: ${preview.repoRoot}`);
|
|
440
|
+
lines.push(`Repo identity source: ${preview.repoIdentitySource}`);
|
|
441
|
+
lines.push(`Source branch: ${preview.sourceBranch}`);
|
|
442
|
+
lines.push(`Branch path segment: ${preview.branchKey}`);
|
|
443
|
+
if (preview.modifiedTimeMs !== undefined) {
|
|
444
|
+
lines.push(`Modified: ${new Date(preview.modifiedTimeMs).toISOString()}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
lines.push("");
|
|
448
|
+
lines.push("Target:");
|
|
449
|
+
lines.push(`Branch: ${preview.targetBranch}`);
|
|
450
|
+
lines.push(`Branch creation: ${preview.branchCreation}`);
|
|
451
|
+
lines.push("Attach plan as:");
|
|
452
|
+
lines.push(`Branch Memory namespace: ${BRANCH_CONTEXT_NAMESPACE}`);
|
|
453
|
+
lines.push(`Branch Memory key: ${preview.planKey}`);
|
|
454
|
+
return lines.join("\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function deriveImplCurrentSavedPlanPreview(
|
|
458
|
+
selected: SelectedSavedPlanFile,
|
|
459
|
+
): Promise<ImplCurrentSavedPlanPreview> {
|
|
460
|
+
const selectedFile = selectedSavedPlanFileInfo(selected);
|
|
461
|
+
const planContent = await readFile(selectedFile.filePath, "utf8");
|
|
462
|
+
const base = {
|
|
463
|
+
savedPlanFileStem: selected.savedPlanFileStem,
|
|
464
|
+
filePath: selectedFile.filePath,
|
|
465
|
+
fileName: selectedFile.fileName,
|
|
466
|
+
planContent,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
return { ...base, ...selectedSavedPlanEvidence(selected) };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function formatImplCurrentSavedPlanEvidence(preview: ImplCurrentSavedPlanPreview): string {
|
|
473
|
+
const lines = [
|
|
474
|
+
preview.mode === "explicit"
|
|
475
|
+
? "Explicit saved plan file:"
|
|
476
|
+
: preview.mode === "session"
|
|
477
|
+
? "Saved plan from current session:"
|
|
478
|
+
: "Latest saved plan from local plan store:",
|
|
479
|
+
];
|
|
480
|
+
lines.push(`Path: ${preview.filePath}`);
|
|
481
|
+
lines.push(`File name: ${preview.fileName}`);
|
|
482
|
+
lines.push(`Saved-plan file stem: ${preview.savedPlanFileStem}`);
|
|
483
|
+
lines.push(`Selection mode: ${preview.mode}`);
|
|
484
|
+
lines.push("Current branch: inherited from the active Pi session; no checkout is performed.");
|
|
485
|
+
if (preview.mode !== "explicit") {
|
|
486
|
+
lines.push(`Repo key: ${preview.repoKey}`);
|
|
487
|
+
lines.push(`Repo root: ${preview.repoRoot}`);
|
|
488
|
+
lines.push(`Repo identity source: ${preview.repoIdentitySource}`);
|
|
489
|
+
lines.push(`Source branch: ${preview.sourceBranch}`);
|
|
490
|
+
lines.push(`Branch path segment: ${preview.branchKey}`);
|
|
491
|
+
if (preview.modifiedTimeMs !== undefined) {
|
|
492
|
+
lines.push(`Modified: ${new Date(preview.modifiedTimeMs).toISOString()}`);
|
|
493
|
+
}
|
|
494
|
+
if (preview.summary !== undefined) {
|
|
495
|
+
lines.push(`Summary: ${preview.summary}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return lines.join("\n");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function buildImplCurrentSavedPlanPrompt(preview: ImplCurrentSavedPlanPreview): string {
|
|
502
|
+
return `# Saved Plan implementation
|
|
503
|
+
|
|
504
|
+
A saved plan has been selected for implementation on the current branch. No Branch Context was created, no branch checkout was requested, and no Attached Plan was written.
|
|
505
|
+
|
|
506
|
+
${formatImplCurrentSavedPlanEvidence(preview)}
|
|
507
|
+
|
|
508
|
+
## Implementation rules
|
|
509
|
+
|
|
510
|
+
- Create an implementation checklist before editing.
|
|
511
|
+
- Treat the embedded Saved Plan as authoritative unless current repo state proves it stale.
|
|
512
|
+
- If the plan is ambiguous or internally inconsistent, quote the ambiguity and ask for clarification instead of guessing.
|
|
513
|
+
- Follow normal project rules: read before editing, use precise edits, run relevant validation, and do not commit, push, submit, or publish unless the user explicitly asks.
|
|
514
|
+
- Do not call brmem put, brmem copy, brmem delete, or any mutating Branch Memory command merely because this command lives near Branch Context code. If the plan asks for Branch Memory mutation, stop and ask the user.
|
|
515
|
+
- If the Saved Plan includes current-state excerpts, scope boundaries, verification gates, or STOP conditions, compare excerpts against live repo state before editing. An excerpt mismatch is a STOP.
|
|
516
|
+
- If those contract sections are absent, explicitly recognize the plan as old-format/pre-contract and do not invent gates or half-apply excerpt checks.
|
|
517
|
+
- Stop and report instead of guessing on universal STOP triggers: excerpt mismatch; ambiguity or internal inconsistency; a verification gate fails twice after reasonable local attempts; implementation requires touching an out-of-scope file/area; or the plan asks for mutating Branch Memory.
|
|
518
|
+
- Before finishing, compare changed files to the plan's scope. Note autofixer-only formatting outside scope separately; intentional executor edits outside scope require user approval.
|
|
519
|
+
- Report implemented changes, files changed/tree state, validation results, plan deviations, unresolved follow-up, and for any STOP: observed vs expected plus the exact gate/assumption that failed.
|
|
520
|
+
|
|
521
|
+
----- BEGIN SAVED PLAN -----
|
|
522
|
+
${preview.planContent}
|
|
523
|
+
----- END SAVED PLAN -----`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export async function handleImplBranchContextCommand(
|
|
527
|
+
pi: ExtensionAPI,
|
|
528
|
+
args: string,
|
|
529
|
+
ctx: CommandContext,
|
|
530
|
+
options: BranchContextExtensionOptions,
|
|
531
|
+
): Promise<void> {
|
|
532
|
+
const trimmedArgs = args.trim();
|
|
533
|
+
sendCommandProgressOrNotify({
|
|
534
|
+
host: pi,
|
|
535
|
+
ctx,
|
|
536
|
+
message: "Loading attached branch-context plan…",
|
|
537
|
+
});
|
|
538
|
+
await ctx.waitForIdle();
|
|
539
|
+
setRuntimeStatus(ctx, IMPL_BRANCH_CONTEXT_STATUS_KEY, "loading attached branch-context plan…");
|
|
540
|
+
try {
|
|
541
|
+
const operations = resolveBranchContextOperations(options);
|
|
542
|
+
const params = trimmedArgs.length > 0 ? { requestedKey: trimmedArgs } : {};
|
|
543
|
+
const plan = await operations.loadBranchContextPlan(pi, params, {
|
|
544
|
+
cwd: ctx.cwd,
|
|
545
|
+
context: resolveBranchContextContext(pi, ctx.cwd, options),
|
|
546
|
+
...optionalEntry("planStoreRoot", resolvePlanStoreRootOption(options)),
|
|
547
|
+
sessionEntries: ctx.sessionManager?.getBranch?.() ?? [],
|
|
548
|
+
});
|
|
549
|
+
presentBranchContextMessage(
|
|
550
|
+
pi,
|
|
551
|
+
ctx,
|
|
552
|
+
formatLoadedAttachedPlanEvidence(plan),
|
|
553
|
+
{ status: "loaded-plan" },
|
|
554
|
+
"info",
|
|
555
|
+
);
|
|
556
|
+
pi.sendUserMessage(buildImplBranchContextPrompt(plan));
|
|
557
|
+
} catch (error) {
|
|
558
|
+
presentBranchContextFailure(pi, ctx, "Failed to load branch-context plan.", error);
|
|
559
|
+
} finally {
|
|
560
|
+
setRuntimeStatus(ctx, IMPL_BRANCH_CONTEXT_STATUS_KEY, undefined);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export async function handleImplCurrentSavedPlanCommand(
|
|
565
|
+
pi: ExtensionAPI,
|
|
566
|
+
rawArgs: string,
|
|
567
|
+
ctx: CommandContext,
|
|
568
|
+
options: BranchContextExtensionOptions,
|
|
569
|
+
): Promise<void> {
|
|
570
|
+
let args: ImplCurrentSavedPlanArgs;
|
|
571
|
+
try {
|
|
572
|
+
args = parseImplCurrentSavedPlanArgs(rawArgs);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (error instanceof CreateBranchContextUsageError) {
|
|
575
|
+
presentBranchContextMessage(
|
|
576
|
+
pi,
|
|
577
|
+
ctx,
|
|
578
|
+
`Usage error: ${error.message}\n\n${IMPL_CURRENT_SAVED_PLAN_USAGE}`,
|
|
579
|
+
{ status: "usage" },
|
|
580
|
+
"error",
|
|
581
|
+
);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (args.help) {
|
|
588
|
+
await ctx.waitForIdle();
|
|
589
|
+
presentBranchContextMessage(
|
|
590
|
+
pi,
|
|
591
|
+
ctx,
|
|
592
|
+
IMPL_CURRENT_SAVED_PLAN_USAGE,
|
|
593
|
+
{ status: "usage" },
|
|
594
|
+
"info",
|
|
595
|
+
);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
sendCommandProgressOrNotify({
|
|
600
|
+
host: pi,
|
|
601
|
+
ctx,
|
|
602
|
+
message: "Finding saved plan for current-branch implementation…",
|
|
603
|
+
});
|
|
604
|
+
await ctx.waitForIdle();
|
|
605
|
+
|
|
606
|
+
let selected: SelectedSavedPlanFile;
|
|
607
|
+
setRuntimeStatus(ctx, IMPL_CURRENT_SAVED_PLAN_STATUS_KEY, "finding saved plan…");
|
|
608
|
+
try {
|
|
609
|
+
selected = await resolveSelectedSavedPlanFile(pi, args, ctx, options);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
setRuntimeStatus(ctx, IMPL_CURRENT_SAVED_PLAN_STATUS_KEY, undefined);
|
|
612
|
+
presentBranchContextFailure(pi, ctx, "Failed to resolve saved plan file.", error);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
let preview: ImplCurrentSavedPlanPreview;
|
|
617
|
+
setRuntimeStatus(ctx, IMPL_CURRENT_SAVED_PLAN_STATUS_KEY, "reading saved plan…");
|
|
618
|
+
try {
|
|
619
|
+
preview = await deriveImplCurrentSavedPlanPreview(selected);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
setRuntimeStatus(ctx, IMPL_CURRENT_SAVED_PLAN_STATUS_KEY, undefined);
|
|
622
|
+
presentBranchContextFailure(pi, ctx, "Failed to read saved plan file.", error);
|
|
623
|
+
return;
|
|
624
|
+
} finally {
|
|
625
|
+
setRuntimeStatus(ctx, IMPL_CURRENT_SAVED_PLAN_STATUS_KEY, undefined);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const evidence = formatImplCurrentSavedPlanEvidence(preview);
|
|
629
|
+
const prompt = buildImplCurrentSavedPlanPrompt(preview);
|
|
630
|
+
if (args.dryRun) {
|
|
631
|
+
presentBranchContextMessage(
|
|
632
|
+
pi,
|
|
633
|
+
ctx,
|
|
634
|
+
`Dry run: no branch would be created, no plan would be attached, no checkout would happen, no new session would be started, and no implementation prompt would be sent.\n\n${evidence}\n\nNew-session implementation flow:\n/new\n/${IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME} ${preview.filePath}`,
|
|
635
|
+
{ status: "dry-run", targetBranch: "current branch", key: preview.savedPlanFileStem },
|
|
636
|
+
"info",
|
|
637
|
+
);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
presentBranchContextMessage(
|
|
642
|
+
pi,
|
|
643
|
+
ctx,
|
|
644
|
+
`Starting current-branch saved-plan implementation session.\n\n${evidence}`,
|
|
645
|
+
{ status: "loaded-plan" },
|
|
646
|
+
"info",
|
|
647
|
+
);
|
|
648
|
+
const launchResult = await runImplCurrentSavedPlanLaunch({ ctx, prompt });
|
|
649
|
+
if (launchResult.type === "launched") {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (launchResult.type === "cancelled") {
|
|
653
|
+
presentBranchContextMessage(
|
|
654
|
+
pi,
|
|
655
|
+
ctx,
|
|
656
|
+
formatImplCurrentSavedPlanCancelledMessage(preview.filePath),
|
|
657
|
+
{ status: "cancelled" },
|
|
658
|
+
"warning",
|
|
659
|
+
);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
presentBranchContextFailure(
|
|
664
|
+
pi,
|
|
665
|
+
ctx,
|
|
666
|
+
"Selected the saved plan, but failed to start the implementation session.",
|
|
667
|
+
launchResult.message,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export async function handleCreateBranchContextCommand(
|
|
672
|
+
pi: ExtensionAPI,
|
|
673
|
+
rawArgs: string,
|
|
674
|
+
ctx: CommandContext,
|
|
675
|
+
options: BranchContextExtensionOptions,
|
|
676
|
+
): Promise<void> {
|
|
677
|
+
await runCreateBranchContextCommand({
|
|
678
|
+
pi,
|
|
679
|
+
rawArgs,
|
|
680
|
+
ctx,
|
|
681
|
+
extensionOptions: options,
|
|
682
|
+
usage: CREATE_BRANCH_CONTEXT_USAGE,
|
|
683
|
+
statusKey: BRANCH_CONTEXT_STATUS_KEY,
|
|
684
|
+
progressMessage: "Finding saved plan for branch context…",
|
|
685
|
+
derivePreviewOptions: (extensionOptions) => extensionOptions,
|
|
686
|
+
formatDryRunMessage: (preview) =>
|
|
687
|
+
`Dry run: no branch was created and no plan was attached.\n\n${formatCreateBranchContextPreview(preview)}`,
|
|
688
|
+
onCreated: (evidence) => {
|
|
689
|
+
presentBranchContextMessage(
|
|
690
|
+
pi,
|
|
691
|
+
ctx,
|
|
692
|
+
formatBranchContextEvidence(evidence),
|
|
693
|
+
{ status: "success", evidence },
|
|
694
|
+
"info",
|
|
695
|
+
);
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export async function handleGtUpstackImplCommand(
|
|
701
|
+
pi: ExtensionAPI,
|
|
702
|
+
rawArgs: string,
|
|
703
|
+
ctx: CommandContext,
|
|
704
|
+
options: BranchContextExtensionOptions,
|
|
705
|
+
): Promise<void> {
|
|
706
|
+
await runCreateBranchContextCommand({
|
|
707
|
+
pi,
|
|
708
|
+
rawArgs,
|
|
709
|
+
ctx,
|
|
710
|
+
extensionOptions: options,
|
|
711
|
+
usage: GT_UPSTACK_IMPL_USAGE,
|
|
712
|
+
statusKey: GT_UPSTACK_IMPL_STATUS_KEY,
|
|
713
|
+
progressMessage: "Finding saved plan for upstack branch-context implementation…",
|
|
714
|
+
derivePreviewOptions: (extensionOptions) => ({
|
|
715
|
+
...extensionOptions,
|
|
716
|
+
branchContextDefaultCreation: "graphite",
|
|
717
|
+
}),
|
|
718
|
+
formatDryRunMessage: (preview) =>
|
|
719
|
+
formatGtUpstackImplDryRunMessage(
|
|
720
|
+
formatCreateBranchContextPreview(preview),
|
|
721
|
+
preview.targetBranch,
|
|
722
|
+
preview.planKey,
|
|
723
|
+
),
|
|
724
|
+
handleSelectedPlanError: async (args, error) => {
|
|
725
|
+
if (!(error instanceof NoSavedPlanAvailableError)) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
await handleGtUpstackImplExistingReuse({
|
|
729
|
+
pi,
|
|
730
|
+
args,
|
|
731
|
+
ctx,
|
|
732
|
+
originalError: error,
|
|
733
|
+
extensionOptions: options,
|
|
734
|
+
});
|
|
735
|
+
return true;
|
|
736
|
+
},
|
|
737
|
+
onCreated: async (evidence) => {
|
|
738
|
+
await runGtUpstackImplLaunchTail({
|
|
739
|
+
pi,
|
|
740
|
+
ctx,
|
|
741
|
+
mode: "created",
|
|
742
|
+
target: evidence,
|
|
743
|
+
successBody: formatBranchContextEvidence(evidence),
|
|
744
|
+
outputDetails: { status: "success", evidence },
|
|
745
|
+
});
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function runCreateBranchContextCommand(
|
|
751
|
+
commandOptions: RunCreateBranchContextCommandOptions,
|
|
752
|
+
): Promise<void> {
|
|
753
|
+
const { pi, rawArgs, ctx, extensionOptions } = commandOptions;
|
|
754
|
+
let args: CreateBranchContextArgs;
|
|
755
|
+
try {
|
|
756
|
+
args = parseCreateBranchContextArgs(rawArgs);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (error instanceof CreateBranchContextUsageError) {
|
|
759
|
+
presentBranchContextMessage(
|
|
760
|
+
pi,
|
|
761
|
+
ctx,
|
|
762
|
+
`Usage error: ${error.message}\n\n${commandOptions.usage}`,
|
|
763
|
+
{ status: "usage" },
|
|
764
|
+
"error",
|
|
765
|
+
);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (args.help) {
|
|
772
|
+
await ctx.waitForIdle();
|
|
773
|
+
presentBranchContextMessage(pi, ctx, commandOptions.usage, { status: "usage" }, "info");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
sendCommandProgressOrNotify({
|
|
778
|
+
host: pi,
|
|
779
|
+
ctx,
|
|
780
|
+
message: commandOptions.progressMessage,
|
|
781
|
+
});
|
|
782
|
+
await ctx.waitForIdle();
|
|
783
|
+
|
|
784
|
+
const previewOptions = commandOptions.derivePreviewOptions(extensionOptions);
|
|
785
|
+
let selected: SelectedSavedPlanFile;
|
|
786
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, "finding saved plan…");
|
|
787
|
+
try {
|
|
788
|
+
selected = await resolveCreateBranchContextPlanFile(pi, args, ctx, previewOptions);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, undefined);
|
|
791
|
+
if ((await commandOptions.handleSelectedPlanError?.(args, error)) === true) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
presentBranchContextFailure(
|
|
795
|
+
pi,
|
|
796
|
+
ctx,
|
|
797
|
+
"Failed to resolve saved plan file or derive branch slug.",
|
|
798
|
+
error,
|
|
799
|
+
);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let preview: CreateBranchContextPreview;
|
|
804
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, "deriving branch slug from plan content…");
|
|
805
|
+
try {
|
|
806
|
+
preview = await deriveCreateBranchContextPreview(pi, args, ctx, selected, previewOptions);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, undefined);
|
|
809
|
+
presentBranchContextFailure(
|
|
810
|
+
pi,
|
|
811
|
+
ctx,
|
|
812
|
+
"Failed to resolve saved plan file or derive branch slug.",
|
|
813
|
+
error,
|
|
814
|
+
);
|
|
815
|
+
return;
|
|
816
|
+
} finally {
|
|
817
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, undefined);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (args.dryRun) {
|
|
821
|
+
presentBranchContextMessage(
|
|
822
|
+
pi,
|
|
823
|
+
ctx,
|
|
824
|
+
commandOptions.formatDryRunMessage(preview),
|
|
825
|
+
{ status: "dry-run", targetBranch: preview.targetBranch, key: preview.planKey },
|
|
826
|
+
"info",
|
|
827
|
+
);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, "creating branch and attaching plan…");
|
|
832
|
+
let evidence: BranchContextEvidence;
|
|
833
|
+
try {
|
|
834
|
+
evidence = await createBranchContextFromPreview({
|
|
835
|
+
pi,
|
|
836
|
+
preview,
|
|
837
|
+
ctx,
|
|
838
|
+
operations: resolveBranchContextOperations(extensionOptions),
|
|
839
|
+
extensionOptions,
|
|
840
|
+
});
|
|
841
|
+
} catch (error) {
|
|
842
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, undefined);
|
|
843
|
+
presentBranchContextFailure(
|
|
844
|
+
pi,
|
|
845
|
+
ctx,
|
|
846
|
+
"Failed to create branch context and attach the plan.",
|
|
847
|
+
error,
|
|
848
|
+
);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
setRuntimeStatus(ctx, commandOptions.statusKey, undefined);
|
|
853
|
+
await commandOptions.onCreated(evidence);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
interface CreateBranchContextFromPreviewOptions {
|
|
857
|
+
pi: ExtensionAPI;
|
|
858
|
+
preview: CreateBranchContextPreview;
|
|
859
|
+
ctx: CommandContext;
|
|
860
|
+
operations: BranchContextOperations;
|
|
861
|
+
extensionOptions: BranchContextExtensionOptions;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
interface HandleGtUpstackImplExistingReuseOptions {
|
|
865
|
+
pi: ExtensionAPI;
|
|
866
|
+
args: CreateBranchContextArgs;
|
|
867
|
+
ctx: CommandContext;
|
|
868
|
+
originalError: unknown;
|
|
869
|
+
extensionOptions: BranchContextExtensionOptions;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function handleGtUpstackImplExistingReuse(
|
|
873
|
+
options: HandleGtUpstackImplExistingReuseOptions,
|
|
874
|
+
): Promise<void> {
|
|
875
|
+
const { pi, args, ctx, originalError, extensionOptions } = options;
|
|
876
|
+
let reuse: ExistingBranchContextReuse;
|
|
877
|
+
setRuntimeStatus(ctx, GT_UPSTACK_IMPL_STATUS_KEY, "finding existing branch context…");
|
|
878
|
+
try {
|
|
879
|
+
const sessionEntries = ctx.sessionManager?.getBranch?.() ?? [];
|
|
880
|
+
reuse = await resolveExistingBranchContextReuse(
|
|
881
|
+
pi,
|
|
882
|
+
args.branchName === undefined
|
|
883
|
+
? { sessionEntries }
|
|
884
|
+
: { explicitBranch: args.branchName, sessionEntries },
|
|
885
|
+
{ cwd: ctx.cwd, context: resolveBranchContextContext(pi, ctx.cwd, extensionOptions) },
|
|
886
|
+
);
|
|
887
|
+
} catch (reuseError) {
|
|
888
|
+
presentBranchContextMessage(
|
|
889
|
+
pi,
|
|
890
|
+
ctx,
|
|
891
|
+
formatExistingReuseFailureMessage(originalError, reuseError),
|
|
892
|
+
{ status: "failure", error: formatErrorMessage(reuseError) },
|
|
893
|
+
"error",
|
|
894
|
+
);
|
|
895
|
+
return;
|
|
896
|
+
} finally {
|
|
897
|
+
setRuntimeStatus(ctx, GT_UPSTACK_IMPL_STATUS_KEY, undefined);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (args.dryRun) {
|
|
901
|
+
presentBranchContextMessage(
|
|
902
|
+
pi,
|
|
903
|
+
ctx,
|
|
904
|
+
formatGtUpstackImplDryRunMessage(
|
|
905
|
+
formatExistingBranchContextReuse(reuse),
|
|
906
|
+
reuse.branch,
|
|
907
|
+
reuse.key,
|
|
908
|
+
),
|
|
909
|
+
{ status: "dry-run", targetBranch: reuse.branch, key: reuse.key },
|
|
910
|
+
"info",
|
|
911
|
+
);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
await runGtUpstackImplLaunchTail({
|
|
916
|
+
pi,
|
|
917
|
+
ctx,
|
|
918
|
+
mode: "reused",
|
|
919
|
+
target: reuse,
|
|
920
|
+
successBody: `Reusing existing branch context and attached plan.\n\n${formatExistingBranchContextReuse(reuse)}`,
|
|
921
|
+
outputDetails: { status: "reuse" },
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function createBranchContextFromPreview({
|
|
926
|
+
pi,
|
|
927
|
+
preview,
|
|
928
|
+
ctx,
|
|
929
|
+
operations,
|
|
930
|
+
extensionOptions,
|
|
931
|
+
}: CreateBranchContextFromPreviewOptions): Promise<BranchContextEvidence> {
|
|
932
|
+
const params: {
|
|
933
|
+
slug: string;
|
|
934
|
+
filePath: string;
|
|
935
|
+
branchCreation: BranchCreationMethod;
|
|
936
|
+
branchName?: string;
|
|
937
|
+
summary?: string;
|
|
938
|
+
} = {
|
|
939
|
+
slug: preview.slug,
|
|
940
|
+
filePath: preview.filePath,
|
|
941
|
+
branchCreation: preview.branchCreation,
|
|
942
|
+
};
|
|
943
|
+
if (preview.branchNameForCreation !== undefined) {
|
|
944
|
+
params.branchName = preview.branchNameForCreation;
|
|
945
|
+
}
|
|
946
|
+
if (preview.summary !== undefined) {
|
|
947
|
+
params.summary = preview.summary;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return operations.createBranchContextFromFile(pi, params, {
|
|
951
|
+
cwd: ctx.cwd,
|
|
952
|
+
context: resolveBranchContextContext(pi, ctx.cwd, extensionOptions),
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function formatExistingReuseFailureMessage(originalError: unknown, reuseError: unknown): string {
|
|
957
|
+
return [
|
|
958
|
+
"Failed to resolve saved plan file or derive branch slug.",
|
|
959
|
+
"",
|
|
960
|
+
"Original saved-plan resolution failure:",
|
|
961
|
+
formatErrorMessage(originalError),
|
|
962
|
+
"",
|
|
963
|
+
"Existing branch-context reuse failure:",
|
|
964
|
+
formatErrorMessage(reuseError),
|
|
965
|
+
].join("\n");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
type GtUpstackImplMode = "created" | "reused";
|
|
969
|
+
|
|
970
|
+
type ImplCurrentSavedPlanLaunchResult =
|
|
971
|
+
| { type: "launched"; parentSession?: string }
|
|
972
|
+
| { type: "cancelled"; parentSession?: string }
|
|
973
|
+
| { type: "failed"; message: string; parentSession?: string };
|
|
974
|
+
|
|
975
|
+
interface ImplCurrentSavedPlanLaunchOptions {
|
|
976
|
+
ctx: CommandContext;
|
|
977
|
+
prompt: string;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
interface GtUpstackImplLaunchTailOptions {
|
|
981
|
+
pi: ExtensionAPI;
|
|
982
|
+
ctx: CommandContext;
|
|
983
|
+
mode: GtUpstackImplMode;
|
|
984
|
+
target: Pick<BranchContextEvidence, "branch" | "key">;
|
|
985
|
+
successBody: string;
|
|
986
|
+
outputDetails: BranchContextOutputDetails;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function runGtUpstackImplLaunchTail(options: GtUpstackImplLaunchTailOptions): Promise<void> {
|
|
990
|
+
const { pi, ctx, mode, target } = options;
|
|
991
|
+
presentBranchContextMessage(pi, ctx, options.successBody, options.outputDetails, "info");
|
|
992
|
+
|
|
993
|
+
const launchResult = await runBranchContextGtUpstackImplLaunch({
|
|
994
|
+
host: pi,
|
|
995
|
+
ctx,
|
|
996
|
+
statusKey: GT_UPSTACK_IMPL_STATUS_KEY,
|
|
997
|
+
target,
|
|
998
|
+
});
|
|
999
|
+
if (launchResult.type === "launched") {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (launchResult.type === "cancelled") {
|
|
1003
|
+
presentBranchContextMessage(
|
|
1004
|
+
pi,
|
|
1005
|
+
ctx,
|
|
1006
|
+
formatGtUpstackImplCancelledMessage(mode, launchResult.branch, launchResult.key),
|
|
1007
|
+
{ status: "cancelled" },
|
|
1008
|
+
"warning",
|
|
1009
|
+
);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
presentBranchContextFailure(
|
|
1014
|
+
pi,
|
|
1015
|
+
ctx,
|
|
1016
|
+
formatGtUpstackImplLaunchFailureTitle(mode, launchResult.phase),
|
|
1017
|
+
launchResult.message,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function runImplCurrentSavedPlanLaunch(
|
|
1022
|
+
options: ImplCurrentSavedPlanLaunchOptions,
|
|
1023
|
+
): Promise<ImplCurrentSavedPlanLaunchResult> {
|
|
1024
|
+
let isReplacementSessionActive = false;
|
|
1025
|
+
let parentSession: string | undefined;
|
|
1026
|
+
try {
|
|
1027
|
+
setRuntimeStatus(
|
|
1028
|
+
options.ctx,
|
|
1029
|
+
IMPL_CURRENT_SAVED_PLAN_STATUS_KEY,
|
|
1030
|
+
"starting implementation session…",
|
|
1031
|
+
);
|
|
1032
|
+
parentSession = options.ctx.sessionManager?.getSessionFile?.();
|
|
1033
|
+
const newSessionOptions = {
|
|
1034
|
+
...optionalEntry("parentSession", parentSession),
|
|
1035
|
+
withSession: async (newCtx: ReplacedSessionContext) => {
|
|
1036
|
+
isReplacementSessionActive = true;
|
|
1037
|
+
await newCtx.sendUserMessage(options.prompt);
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
const result = await options.ctx.newSession(newSessionOptions);
|
|
1041
|
+
if (result.cancelled) {
|
|
1042
|
+
return {
|
|
1043
|
+
type: "cancelled",
|
|
1044
|
+
...optionalEntry("parentSession", parentSession),
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return { type: "launched", ...optionalEntry("parentSession", parentSession) };
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
if (isReplacementSessionActive) {
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
type: "failed",
|
|
1054
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1055
|
+
...optionalEntry("parentSession", parentSession),
|
|
1056
|
+
};
|
|
1057
|
+
} finally {
|
|
1058
|
+
if (!isReplacementSessionActive) {
|
|
1059
|
+
setRuntimeStatus(options.ctx, IMPL_CURRENT_SAVED_PLAN_STATUS_KEY, undefined);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function formatGtUpstackImplDryRunMessage(body: string, branch: string, key: string): string {
|
|
1065
|
+
return `Dry run: no branch would be created, no plan would be attached, no checkout would happen, no new session would be started, and no implementation prompt would be sent.\n\n${body}\n\nNew-session implementation flow:\n${formatBranchContextGtUpstackImplFollowUpFlow(branch, key)}`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function formatImplCurrentSavedPlanCancelledMessage(filePath: string): string {
|
|
1069
|
+
return `Selected the saved plan, but starting the implementation session was cancelled. Run /${IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME} ${filePath} again, or manually open /new on the current branch and paste/use the saved plan content.`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function formatGtUpstackImplLaunchFailureTitle(
|
|
1073
|
+
mode: GtUpstackImplMode,
|
|
1074
|
+
phase: "checkout" | "new-session",
|
|
1075
|
+
): string {
|
|
1076
|
+
if (mode === "created") {
|
|
1077
|
+
return phase === "checkout"
|
|
1078
|
+
? "Created branch context and attached the plan, but failed to check out the branch context."
|
|
1079
|
+
: "Created branch context, attached the plan, and checked out the branch context, but failed to start the implementation session.";
|
|
1080
|
+
}
|
|
1081
|
+
return phase === "checkout"
|
|
1082
|
+
? "Reused existing branch context and attached plan, but failed to check out the branch context."
|
|
1083
|
+
: "Reused existing branch context, verified the attached plan, and checked out the branch context, but failed to start the implementation session.";
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function formatGtUpstackImplCancelledMessage(
|
|
1087
|
+
mode: GtUpstackImplMode,
|
|
1088
|
+
branch: string,
|
|
1089
|
+
key: string,
|
|
1090
|
+
): string {
|
|
1091
|
+
const command = formatImplBranchContextCommand(key);
|
|
1092
|
+
if (mode === "created") {
|
|
1093
|
+
return `Created branch context, attached the plan, and checked out ${branch}, but starting the implementation session was cancelled. Run ${command} to continue.`;
|
|
1094
|
+
}
|
|
1095
|
+
return `Reused existing branch context, verified the attached plan, and checked out ${branch}, but starting the implementation session was cancelled. Run ${command} to continue.`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function resolveSelectedSavedPlanFile(
|
|
1099
|
+
pi: ExtensionAPI,
|
|
1100
|
+
args: { filePath?: string },
|
|
1101
|
+
ctx: CommandContext,
|
|
1102
|
+
options: BranchContextExtensionOptions,
|
|
1103
|
+
): Promise<SelectedSavedPlanFile> {
|
|
1104
|
+
const operations = resolveBranchContextOperations(options);
|
|
1105
|
+
const planStoreRoot = resolvePlanStoreRootOption(options);
|
|
1106
|
+
return operations.resolveSelectedSavedPlanFile(pi, {
|
|
1107
|
+
cwd: ctx.cwd,
|
|
1108
|
+
...optionalEntries({ planStoreRoot, explicitPath: args.filePath }),
|
|
1109
|
+
sessionEntries: ctx.sessionManager?.getBranch?.() ?? [],
|
|
1110
|
+
shouldFallbackToLatest: true,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function selectedSavedPlanFileInfo(selected: SelectedSavedPlanFile): {
|
|
1115
|
+
filePath: string;
|
|
1116
|
+
fileName: string;
|
|
1117
|
+
} {
|
|
1118
|
+
if (selected.type === "explicit") {
|
|
1119
|
+
return { filePath: selected.filePath, fileName: selected.fileName };
|
|
1120
|
+
}
|
|
1121
|
+
return { filePath: selected.plan.filePath, fileName: selected.plan.fileName };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function selectedSavedPlanEvidence(selected: SelectedSavedPlanFile): SelectedSavedPlanEvidence {
|
|
1125
|
+
if (selected.type === "explicit") {
|
|
1126
|
+
return { mode: "explicit" };
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
mode: selected.type,
|
|
1130
|
+
repoRoot: selected.plan.repoRoot,
|
|
1131
|
+
repoKey: selected.plan.repoKey,
|
|
1132
|
+
repoIdentitySource: selected.plan.repoIdentitySource,
|
|
1133
|
+
sourceBranch: selected.plan.sourceBranch,
|
|
1134
|
+
branchKey: selected.plan.branchKey,
|
|
1135
|
+
modifiedTimeMs: selected.plan.modifiedTimeMs,
|
|
1136
|
+
...(selected.type === "session" && selected.plan.summary !== undefined
|
|
1137
|
+
? { summary: selected.plan.summary }
|
|
1138
|
+
: {}),
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function deriveBranchContextTargetBranch(
|
|
1143
|
+
args: CreateBranchContextArgs,
|
|
1144
|
+
slug: string,
|
|
1145
|
+
options: BranchContextExtensionOptions,
|
|
1146
|
+
): BranchContextTargetBranch {
|
|
1147
|
+
if (args.branchName !== undefined) {
|
|
1148
|
+
const targetBranch = deriveTargetBranch(args.branchName, slug);
|
|
1149
|
+
return { targetBranch, branchNameForCreation: targetBranch, isExplicitTargetBranch: true };
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const prefix = options.branchContextPrefix?.trim();
|
|
1153
|
+
if (prefix !== undefined && prefix.length > 0) {
|
|
1154
|
+
const targetBranch = `${prefix}${slug}`;
|
|
1155
|
+
return { targetBranch, branchNameForCreation: targetBranch, isExplicitTargetBranch: false };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return { targetBranch: deriveTargetBranch(undefined, slug), isExplicitTargetBranch: false };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function presentBranchContextFailure(
|
|
1162
|
+
pi: ExtensionAPI,
|
|
1163
|
+
ctx: CommandContext,
|
|
1164
|
+
title: string,
|
|
1165
|
+
error: unknown,
|
|
1166
|
+
): void {
|
|
1167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1168
|
+
presentBranchContextMessage(
|
|
1169
|
+
pi,
|
|
1170
|
+
ctx,
|
|
1171
|
+
`${title}\n\n${message}`,
|
|
1172
|
+
{ status: "failure", error: message },
|
|
1173
|
+
"error",
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function presentBranchContextMessage(
|
|
1178
|
+
pi: ExtensionAPI,
|
|
1179
|
+
ctx: CommandContext,
|
|
1180
|
+
content: string,
|
|
1181
|
+
details: BranchContextOutputDetails,
|
|
1182
|
+
level: NotifyLevel,
|
|
1183
|
+
): void {
|
|
1184
|
+
if (pi.sendMessage) {
|
|
1185
|
+
pi.sendMessage(buildBranchContextOutputMessage(content, details));
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (ctx.hasUI) {
|
|
1190
|
+
ctx.ui.notify(content, level);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (level === "error") {
|
|
1195
|
+
console.error(content);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
console.log(content);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
export function registerBranchContextCommands(
|
|
1202
|
+
pi: ExtensionAPI,
|
|
1203
|
+
options: BranchContextExtensionOptions = {},
|
|
1204
|
+
): void {
|
|
1205
|
+
registerCommandWithImmediateAck({
|
|
1206
|
+
host: pi,
|
|
1207
|
+
commandName: CREATE_BRANCH_CONTEXT_COMMAND_NAME,
|
|
1208
|
+
commandDefinition: {
|
|
1209
|
+
description:
|
|
1210
|
+
"Create a branch context using a content-derived slug, then attach the saved plan in Branch Memory.",
|
|
1211
|
+
handler: async (args, ctx) => handleCreateBranchContextCommand(pi, args, ctx, options),
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
registerCommandWithImmediateAck({
|
|
1216
|
+
host: pi,
|
|
1217
|
+
commandName: GT_UPSTACK_IMPL_COMMAND_NAME,
|
|
1218
|
+
commandDefinition: {
|
|
1219
|
+
description:
|
|
1220
|
+
"Stack a branch context on the current branch with Graphite, check it out, and implement the attached plan in a fresh Pi session.",
|
|
1221
|
+
handler: async (args, ctx) => handleGtUpstackImplCommand(pi, args, ctx, options),
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
registerCommandWithImmediateAck({
|
|
1226
|
+
host: pi,
|
|
1227
|
+
commandName: IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME,
|
|
1228
|
+
commandDefinition: {
|
|
1229
|
+
description:
|
|
1230
|
+
"Implement a selected saved plan in a fresh Pi session on the current branch without creating branch context.",
|
|
1231
|
+
handler: async (args, ctx) => handleImplCurrentSavedPlanCommand(pi, args, ctx, options),
|
|
1232
|
+
},
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
registerCommandWithImmediateAck({
|
|
1236
|
+
host: pi,
|
|
1237
|
+
commandName: IMPL_BRANCH_CONTEXT_COMMAND_NAME,
|
|
1238
|
+
commandDefinition: {
|
|
1239
|
+
description: "Implement from the attached or latest saved branch-context plan.",
|
|
1240
|
+
handler: async (args, ctx) => handleImplBranchContextCommand(pi, args, ctx, options),
|
|
1241
|
+
},
|
|
1242
|
+
});
|
|
1243
|
+
}
|